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.ANGLE;
52  import static ffx.potential.parameters.ForceField.ForceFieldType.ANGLEP;
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.LocalGeometryFunctionalForm;
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.lang.String.valueOf;
62  import static java.lang.System.arraycopy;
63  import static org.apache.commons.math3.util.FastMath.PI;
64  import static org.apache.commons.math3.util.FastMath.pow;
65  
66  /**
67   * The AngleType class defines one harmonic angle bend energy term.
68   *
69   * @author Michael J. Schnieders
70   * @since 1.0
71   */
72  @FFXProperty(name = "angle", clazz = String.class, propertyGroup = PotentialFunctionParameter, description = """
73      [3 integers and 4 reals]
74      Provides the values for a single bond angle bending parameter.
75      The integer modifiers give the atom class numbers for the three kinds of atoms involved in the angle which is to be defined.
76      The real number modifiers give the force constant value for the angle and up to three ideal bond angles in degrees.
77      In most cases only one ideal bond angle is given, and that value is used for all occurrences of the specified bond angle.
78      If all three ideal angles are given, the values apply when the central atom of the angle is attached to 0, 1 or 2 additional hydrogen atoms, respectively.
79      This "hydrogen environment" option is provided to implement the corresponding feature of the AMOEBA force field.
80      The default units for the force constant are kcal/mole/radian^2, but this can be controlled via the angleunit keyword.
81      """)
82  @FFXProperty(name = "anglep", clazz = String.class, propertyGroup = PotentialFunctionParameter, description = """
83      [3 integers and 3 reals]
84      Provides the values for a single projected in-plane bond angle bending parameter.
85      The integer modifiers give the atom class numbers for the three kinds of atoms involved in the angle which is to be defined.
86      The real number modifiers give the force constant value for the angle and up to two ideal bond angles in degrees.
87      In most cases only one ideal bond angle is given, and that value is used for all occurrences of the specified bond angle.
88      If all two ideal angles are given, the values apply when the central atom of the angle is attached to 0 or 1 additional hydrogen atoms, respectively.
89      This "hydrogen environment" option is provided to implement the corresponding feature of the AMOEBA force field.
90      The default units for the force constant are kcal/mole/radian^2, but this can be controlled via the angleunit keyword.
91      """)
92  public final class AngleType extends BaseType implements Comparator<String> {
93  
94    /**
95     * Default convert angle bending energy to kcal/mole.
96     */
97    public static final double DEFAULT_ANGLE_UNIT = pow(PI / 180.0, 2.0);
98    /**
99     * Default cubic coefficient in angle bending potential.
100    */
101   public static final double DEFAULT_ANGLE_CUBIC = 0.0;
102   /**
103    * Default quartic coefficient in angle bending potential.
104    */
105   public static final double DEFAULT_ANGLE_QUARTIC = 0.0;
106   /**
107    * Default pentic coefficient in angle bending potential.
108    */
109   public static final double DEFAULT_ANGLE_PENTIC = 0.0;
110   /**
111    * Default quintic coefficient in angle bending potential.
112    */
113   public static final double DEFAULT_ANGLE_SEXTIC = 0.0;
114 
115   /**
116    * Convert angle bending energy to kcal/mole.
117    */
118   @FFXProperty(name = "angleunit", propertyGroup = EnergyUnitConversion, defaultValue = "(Pi/180)^2", description = """
119       Sets the scale factor needed to convert the energy value computed by the bond angle bending potential into units of kcal/mole.
120       The correct value is force field dependent and typically provided in the header of the master force field parameter file.
121       """)
122   public double angleUnit = DEFAULT_ANGLE_UNIT;
123 
124   /**
125    * Cubic coefficient in angle bending potential.
126    */
127   @FFXProperty(name = "angle-cubic", propertyGroup = LocalGeometryFunctionalForm, defaultValue = "0.0", description = """
128       Sets the value of the cubic term in the Taylor series expansion form of the bond angle bending potential energy.
129       The real number modifier gives the value of the coefficient as a multiple of the quadratic coefficient.
130       This term multiplied by the angle bending energy unit conversion factor, the force constant,
131       and the cube of the deviation of the bond angle from its ideal value gives the cubic contribution to the angle bending energy.
132       The default value in the absence of the angle-cubic keyword is zero; i.e., the cubic angle bending term is omitted.
133       """)
134   public double cubic = DEFAULT_ANGLE_CUBIC;
135 
136   /**
137    * Quartic coefficient in angle bending potential.
138    */
139   @FFXProperty(name = "angle-quartic", propertyGroup = LocalGeometryFunctionalForm, defaultValue = "0.0", description = """
140       Sets the value of the quartic term in the Taylor series expansion form of the bond angle bending potential energy.
141       The real number modifier gives the value of the coefficient as a multiple of the quadratic coefficient.
142       This term multiplied by the angle bending energy unit conversion factor, the force constant,
143       and the forth power of the deviation of the bond angle from its ideal value gives the quartic contribution to the angle bending energy.
144       The default value in the absence of the angle-quartic keyword is zero; i.e., the quartic angle bending term is omitted.
145       """)
146   public double quartic = DEFAULT_ANGLE_QUARTIC;
147 
148   /**
149    * Pentic coefficient in angle bending potential.
150    */
151   @FFXProperty(name = "angle-pentic", propertyGroup = LocalGeometryFunctionalForm, defaultValue = "0.0", description = """
152       Sets the value of the fifth power term in the Taylor series expansion form of the bond angle bending potential energy.
153       The real number modifier gives the value of the coefficient as a multiple of the quadratic coefficient.
154       This term multiplied by the angle bending energy unit conversion factor, the force constant,
155       and the fifth power of the deviation of the bond angle from its ideal value gives the pentic contribution to the angle bending energy.
156       The default value in the absence of the angle-pentic keyword is zero; i.e., the pentic angle bending term is omitted.
157       """)
158   public double pentic = DEFAULT_ANGLE_PENTIC;
159 
160   /**
161    * Sextic coefficient in angle bending potential.
162    */
163   @FFXProperty(name = "angle-sextic", propertyGroup = LocalGeometryFunctionalForm, defaultValue = "0.0", description = """
164       Sets the value of the sixth power term in the Taylor series expansion form of the bond angle bending potential energy.
165       The real number modifier gives the value of the coefficient as a multiple of the quadratic coefficient.
166       This term multiplied by the angle bending energy unit conversion factor, the force constant,
167       and the sixth power of the deviation of the bond angle from its ideal value gives the sextic contribution to the angle bending energy.
168       The default value in the absence of the angle-sextic keyword is zero; i.e., the sextic angle bending term is omitted.
169       """)
170   public double sextic = DEFAULT_ANGLE_SEXTIC;
171 
172   /**
173    * A Logger for the AngleType class.
174    */
175   private static final Logger logger = Logger.getLogger(AngleType.class.getName());
176   /**
177    * Atom classes that for this Angle type.
178    */
179   public final int[] atomClasses;
180   /**
181    * Force constant (Kcal/mole/radian^2).
182    */
183   public final double forceConstant;
184   /**
185    * Equilibrium angle (degrees). There can be up to three equilibrium angles, depending on the
186    * number of attached hydrogens (0, 1, or 2).
187    */
188   public final double[] angle;
189   /**
190    * The angle mode in use.
191    */
192   public final AngleMode angleMode;
193   /**
194    * The angle function in use.
195    */
196   public AngleFunction angleFunction;
197 
198   /**
199    * The default AngleType constructor defines use of the Sextic AngleFunction.
200    *
201    * @param atomClasses   an array of int.
202    * @param forceConstant a double.
203    * @param angle         an array of double.
204    */
205   public AngleType(int[] atomClasses, double forceConstant, double[] angle) {
206     this(atomClasses, forceConstant, angle, AngleMode.NORMAL);
207   }
208 
209   /**
210    * Constructor for In-Plane AngleType.
211    *
212    * @param atomClasses   an array of int.
213    * @param forceConstant a double.
214    * @param angle         an array of double.
215    * @param angleMode     the AngleMode to apply.
216    */
217   public AngleType(int[] atomClasses, double forceConstant, double[] angle, AngleMode angleMode) {
218     this(atomClasses, forceConstant, angle, angleMode, AngleFunction.SEXTIC);
219   }
220 
221   /**
222    * Constructor for In-Plane AngleType.
223    *
224    * @param atomClasses   an array of int.
225    * @param forceConstant a double.
226    * @param angle         an array of double.
227    * @param angleMode     the AngleMode to apply.
228    * @param angleFunction the AngleFunction to use.
229    */
230   public AngleType(int[] atomClasses, double forceConstant, double[] angle, AngleMode angleMode,
231                    AngleFunction angleFunction) {
232     super(ANGLE, sortKey(atomClasses));
233     this.atomClasses = atomClasses;
234     this.forceConstant = forceConstant;
235     this.angle = angle;
236     this.angleMode = angleMode;
237     this.angleFunction = angleFunction;
238     if (angleMode == AngleMode.IN_PLANE) {
239       forceFieldType = ANGLEP;
240     }
241   }
242 
243   /**
244    * Average two AngleType instances. The atom classes that define the new type must be supplied.
245    *
246    * @param angleType1  a {@link ffx.potential.parameters.AngleType} object.
247    * @param angleType2  a {@link ffx.potential.parameters.AngleType} object.
248    * @param atomClasses an array of {@link int} objects.
249    * @return a {@link ffx.potential.parameters.AngleType} object.
250    */
251   public static AngleType average(AngleType angleType1, AngleType angleType2, int[] atomClasses) {
252     if (angleType1 == null || angleType2 == null || atomClasses == null) {
253       return null;
254     }
255     AngleMode angleMode = angleType1.angleMode;
256     if (angleMode != angleType2.angleMode) {
257       return null;
258     }
259     AngleFunction angleFunction = angleType1.angleFunction;
260     if (angleFunction != angleType2.angleFunction) {
261       return null;
262     }
263     int len = angleType1.angle.length;
264     if (len != angleType2.angle.length) {
265       return null;
266     }
267     double forceConstant = (angleType1.forceConstant + angleType2.forceConstant) / 2.0;
268     double[] angle = new double[len];
269     for (int i = 0; i < len; i++) {
270       angle[i] = (angleType1.angle[i] + angleType2.angle[i]) / 2.0;
271     }
272 
273     return new AngleType(atomClasses, forceConstant, angle, angleMode, angleFunction);
274   }
275 
276   /**
277    * Construct an AngleType from an input string.
278    *
279    * @param input  The overall input String.
280    * @param tokens The input String tokenized.
281    * @return an AngleType instance.
282    */
283   public static AngleType parse(String input, String[] tokens) {
284     if (tokens.length < 6) {
285       logger.log(Level.WARNING, "Invalid ANGLE type:\n{0}", input);
286     } else {
287       int[] atomClasses = new int[3];
288       double forceConstant;
289       int angles;
290       double[] bondAngle;
291       try {
292         atomClasses[0] = parseInt(tokens[1]);
293         atomClasses[1] = parseInt(tokens[2]);
294         atomClasses[2] = parseInt(tokens[3]);
295         forceConstant = parseDouble(tokens[4]);
296         angles = tokens.length - 5;
297         bondAngle = new double[angles];
298         for (int i = 0; i < angles; i++) {
299           bondAngle[i] = parseDouble(tokens[5 + i]);
300         }
301       } catch (NumberFormatException e) {
302         String message = "Exception parsing ANGLE type:\n" + input + "\n";
303         logger.log(Level.SEVERE, message, e);
304         return null;
305       }
306       double[] newBondAngle = new double[angles];
307       arraycopy(bondAngle, 0, newBondAngle, 0, angles);
308       return new AngleType(atomClasses, forceConstant, newBondAngle);
309     }
310     return null;
311   }
312 
313   /**
314    * Construct an In-Plane AngleType from an input string.
315    *
316    * @param input  The overall input String.
317    * @param tokens The input String tokenized.
318    * @return an AngleType instance.
319    */
320   public static AngleType parseInPlane(String input, String[] tokens) {
321     if (tokens.length < 6) {
322       logger.log(Level.WARNING, "Invalid ANGLEP type:\n{0}", input);
323     } else {
324       int[] atomClasses = new int[3];
325       double forceConstant;
326       int angles;
327       double[] bondAngle;
328       try {
329         atomClasses[0] = parseInt(tokens[1]);
330         atomClasses[1] = parseInt(tokens[2]);
331         atomClasses[2] = parseInt(tokens[3]);
332         forceConstant = parseDouble(tokens[4]);
333         angles = tokens.length - 5;
334         bondAngle = new double[angles];
335         for (int i = 0; i < angles; i++) {
336           bondAngle[i] = parseDouble(tokens[5 + i]);
337         }
338       } catch (NumberFormatException e) {
339         String message = "Exception parsing ANGLEP type:\n" + input + "\n";
340         logger.log(Level.SEVERE, message, e);
341         return null;
342       }
343       double[] newBondAngle = new double[angles];
344       arraycopy(bondAngle, 0, newBondAngle, 0, angles);
345       return new AngleType(atomClasses, forceConstant, newBondAngle, AngleMode.IN_PLANE);
346     }
347     return null;
348   }
349 
350   /**
351    * This method sorts the atom classes as: min, c[1], max
352    *
353    * @param c atomClasses
354    * @return lookup key
355    */
356   public static String sortKey(int[] c) {
357     if (c == null || c.length != 3) {
358       return null;
359     }
360     if (c[0] > c[2]) {
361       int temp = c[0];
362       c[0] = c[2];
363       c[2] = temp;
364     }
365     return c[0] + " " + c[1] + " " + c[2];
366   }
367 
368   /**
369    * {@inheritDoc}
370    */
371   @Override
372   public int compare(String key1, String key2) {
373     String[] keys1 = key1.split(" ");
374     String[] keys2 = key2.split(" ");
375     int[] c1 = new int[3];
376     int[] c2 = new int[3];
377     for (int i = 0; i < 3; i++) {
378       c1[i] = Integer.parseInt(keys1[i]);
379       c2[i] = Integer.parseInt(keys2[i]);
380     }
381 
382     if (c1[1] < c2[1]) {
383       return -1;
384     } else if (c1[1] > c2[1]) {
385       return 1;
386     } else if (c1[0] < c2[0]) {
387       return -1;
388     } else if (c1[0] > c2[0]) {
389       return 1;
390     } else if (c1[2] < c2[2]) {
391       return -1;
392     } else if (c1[2] > c2[2]) {
393       return 1;
394     }
395 
396     return 0;
397   }
398 
399   /**
400    * {@inheritDoc}
401    */
402   @Override
403   public boolean equals(Object o) {
404     if (this == o) {
405       return true;
406     }
407     if (o == null || getClass() != o.getClass()) {
408       return false;
409     }
410     AngleType angleType = (AngleType) o;
411     return Arrays.equals(atomClasses, angleType.atomClasses);
412   }
413 
414   /**
415    * {@inheritDoc}
416    */
417   @Override
418   public int hashCode() {
419     return Arrays.hashCode(atomClasses);
420   }
421 
422   /**
423    * incrementClasses
424    *
425    * @param increment the value to add to each atom class.
426    */
427   public void incrementClasses(int increment) {
428     for (int i = 0; i < atomClasses.length; i++) {
429       atomClasses[i] += increment;
430     }
431     setKey(sortKey(atomClasses));
432   }
433 
434   /**
435    * Remap new atom classes to known internal ones.
436    *
437    * @param typeMap a lookup between new atom types and known atom types.
438    * @return a {@link ffx.potential.parameters.AngleType} object.
439    */
440   public AngleType patchClasses(HashMap<AtomType, AtomType> typeMap) {
441     int count = 0;
442     int len = atomClasses.length;
443 
444     // Look for new AngleTypes that contain 1 or 2 mapped atom classes.
445     for (AtomType newType : typeMap.keySet()) {
446       for (int atomClass : atomClasses) {
447         if (atomClass == newType.atomClass) {
448           count++;
449         }
450       }
451     }
452 
453     // If found, create a new AngleType that bridges to known classes.
454     if (count == 1 || count == 2) {
455       int[] newClasses = Arrays.copyOf(atomClasses, len);
456       for (AtomType newType : typeMap.keySet()) {
457         for (int i = 0; i < len; i++) {
458           if (atomClasses[i] == newType.atomClass) {
459             AtomType knownType = typeMap.get(newType);
460             newClasses[i] = knownType.atomClass;
461           }
462         }
463       }
464       return new AngleType(newClasses, forceConstant, angle, angleMode, angleFunction);
465     }
466 
467     return null;
468   }
469 
470   /**
471    * Set the AngleFunction.
472    *
473    * @param angleFunction the AngleFunction.
474    */
475   public void setAngleFunction(AngleFunction angleFunction) {
476     this.angleFunction = angleFunction;
477   }
478 
479   /**
480    * {@inheritDoc}
481    *
482    * <p>Nicely formatted Angle bending string.
483    */
484   @Override
485   public String toString() {
486     StringBuilder angleString;
487     if (angleMode == AngleMode.NORMAL) {
488       angleString = new StringBuilder(
489           format("angle  %5d  %5d  %5d  %6.2f", atomClasses[0], atomClasses[1], atomClasses[2],
490               forceConstant));
491     } else {
492       angleString = new StringBuilder(
493           format("anglep  %5d  %5d  %5d  %6.2f", atomClasses[0], atomClasses[1], atomClasses[2],
494               forceConstant));
495     }
496     for (double eq : angle) {
497       angleString.append(format("  %6.2f", eq));
498     }
499     return angleString.toString();
500   }
501 
502   /**
503    * Create an AmoebaAngleForce
504    *
505    * @param doc        the Document instance.
506    * @param forceField the ForceField to define constants from.
507    * @return the AmoebaAngleForce Element.
508    */
509   public static Element getXMLForce(Document doc, ForceField forceField) {
510     Map<String, AngleType> angMap = forceField.getAngleTypes();
511     angMap.putAll(forceField.getAnglepTypes());
512     if (!angMap.values().isEmpty()) {
513       Element node = doc.createElement("AmoebaAngleForce");
514       node.setAttribute("angle-cubic", valueOf(forceField.getDouble("angle-cubic", DEFAULT_ANGLE_CUBIC)));
515       node.setAttribute("angle-quartic", valueOf(forceField.getDouble("angle-quartic", DEFAULT_ANGLE_QUARTIC)));
516       node.setAttribute("angle-pentic", valueOf(forceField.getDouble("angle-pentic", DEFAULT_ANGLE_PENTIC)));
517       node.setAttribute("angle-sextic", valueOf(forceField.getDouble("angle-sextic", DEFAULT_ANGLE_SEXTIC)));
518       for (AngleType angleType : angMap.values()) {
519         node.appendChild(angleType.toXML(doc));
520       }
521       return node;
522     }
523     return null;
524   }
525 
526   /**
527    * Write AngleType to OpenMM XML format.
528    *
529    * @param doc the Document instance.
530    * @return the Angle element.
531    */
532   public Element toXML(Document doc) {
533     Element node = doc.createElement("Angle");
534     node.setAttribute("class1", format("%d", atomClasses[0]));
535     node.setAttribute("class2", format("%d", atomClasses[1]));
536     node.setAttribute("class3", format("%d", atomClasses[2]));
537     // Convert Kcal/mol/radian^2 to KJ/mol/deg^2
538     node.setAttribute("k", format("%f", forceConstant * KCAL_TO_KJ / (DEGREES_PER_RADIAN * DEGREES_PER_RADIAN)));
539     int i = 1;
540     for (double eq : angle) {
541       node.setAttribute(format("angle%d", i), format("%f", eq));
542       i++;
543     }
544     if (angleMode == AngleMode.NORMAL) {
545       node.setAttribute("inPlane", "False");
546     } else {
547       node.setAttribute("inPlane", "True");
548     }
549     return node;
550   }
551 
552   /**
553    * Angle function types include harmonic or sextic.
554    */
555   public enum AngleFunction {
556     HARMONIC, SEXTIC
557   }
558 
559   /**
560    * Angle modes include Normal or In-Plane
561    */
562   public enum AngleMode {
563     NORMAL, IN_PLANE
564   }
565 }