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.potential.bonded;
39  
40  import static java.lang.String.format;
41  import static org.apache.commons.math3.util.FastMath.acos;
42  import static org.apache.commons.math3.util.FastMath.max;
43  import static org.apache.commons.math3.util.FastMath.min;
44  import static org.apache.commons.math3.util.FastMath.sqrt;
45  import static org.apache.commons.math3.util.FastMath.toDegrees;
46  
47  import ffx.numerics.Constraint;
48  import ffx.numerics.atomic.AtomicDoubleArray3D;
49  import ffx.numerics.math.Double3;
50  import ffx.potential.parameters.AngleType;
51  import ffx.potential.parameters.AngleType.AngleMode;
52  import ffx.potential.parameters.AtomType;
53  import ffx.potential.parameters.ForceField;
54  
55  import java.io.Serial;
56  import java.util.ArrayList;
57  import java.util.List;
58  import java.util.logging.Logger;
59  
60  /**
61   * The Angle class represents an angle formed between three linearly bonded atoms.
62   *
63   * @author Michael J. Schnieders
64   * @since 1.0
65   */
66  public class Angle extends BondedTerm {
67  
68    @Serial
69    private static final long serialVersionUID = 1L;
70  
71    private static final Logger logger = Logger.getLogger(Angle.class.getName());
72  
73    /**
74     * Force field parameters to compute the angle bending energy.
75     */
76    public AngleType angleType;
77    /**
78     * Number of hydrogen on the central atom that are not part of this Angle.
79     */
80    public int nh = 0;
81    /**
82     * Scale factor to apply to angle bending.
83     */
84    private double rigidScale = 1.0;
85    /**
86     * Fourth atom for in-plane angles.
87     */
88    private Atom atom4 = null;
89  
90    /**
91     * Angle constructor
92     *
93     * @param b1 Bond that forms one leg of the angle
94     * @param b2 Bond that forms the other leg of the angle
95     */
96    public Angle(Bond b1, Bond b2) {
97      super();
98      Atom a2 = b1.getCommonAtom(b2);
99      Atom a1 = b1.get1_2(a2);
100     Atom a3 = b2.get1_2(a2);
101     b1.setAngleWith(b2);
102     b2.setAngleWith(b1);
103     atoms = new Atom[3];
104     bonds = new Bond[2];
105     atoms[1] = a2;
106     atoms[0] = a1;
107     atoms[2] = a3;
108     bonds[0] = b1;
109     bonds[1] = b2;
110 
111     a1.setAngle(this);
112     a2.setAngle(this);
113     a3.setAngle(this);
114     setID_Key(false);
115   }
116 
117   /**
118    * Log that no AngleType exists.
119    *
120    * @param a1 Atom 1.
121    * @param a2 Atom 2.
122    * @param a3 Atom 3.
123    */
124   public static void logNoAngleType(Atom a1, Atom a2, Atom a3, ForceField forceField) {
125     AtomType atomType1 = a1.getAtomType();
126     AtomType atomType2 = a2.getAtomType();
127     AtomType atomType3 = a3.getAtomType();
128     int c1 = atomType1.atomClass;
129     int c2 = atomType2.atomClass;
130     int c3 = atomType3.atomClass;
131     int[] c = {c1, c2, c3};
132     String key = AngleType.sortKey(c);
133     StringBuilder sb = new StringBuilder(
134         format("No AngleType for key: %s\n %s -> %s\n %s -> %s\n %s -> %s",
135             key, a1, atomType1, a2, atomType2, a3, atomType3));
136     List<AtomType> types1 = forceField.getSimilarAtomTypes(atomType1);
137     List<AtomType> types2 = forceField.getSimilarAtomTypes(atomType2);
138     List<AtomType> types3 = forceField.getSimilarAtomTypes(atomType3);
139     List<AngleType> angleTypes = new ArrayList<>();
140     boolean match = false;
141     for (AtomType type1 : types1) {
142       for (AtomType type2 : types2) {
143         // Similar angle type must have the same class for the central atom.
144         if (type2.atomClass != c2) {
145           continue;
146         }
147         for (AtomType type3 : types3) {
148           // Similar angle type must match at least one class for the distal atoms.
149           if ((type1.atomClass != c1) && (type1.atomClass != c3) &&
150               (type3.atomClass != c1) && (type3.atomClass != c3)) {
151             continue;
152           }
153           AngleType angleType = forceField.getAngleType(type1, type2, type3);
154           if (angleType != null && !angleTypes.contains(angleType)) {
155             if (!match) {
156               match = true;
157               sb.append("\n Similar Angle Types:");
158             }
159             angleTypes.add(angleType);
160             sb.append(format("\n  %s", angleType));
161           }
162         }
163       }
164     }
165     logger.severe(sb.toString());
166   }
167 
168   /**
169    * angleFactory.
170    *
171    * @param b1         a {@link ffx.potential.bonded.Bond} object.
172    * @param b2         a {@link ffx.potential.bonded.Bond} object.
173    * @param forceField a {@link ffx.potential.parameters.ForceField} object.
174    * @return a {@link ffx.potential.bonded.Angle} object.
175    */
176   static Angle angleFactory(Bond b1, Bond b2, ForceField forceField) {
177     Angle newAngle = new Angle(b1, b2);
178     Atom ac = b1.getCommonAtom(b2);
179     Atom a1 = b1.get1_2(ac);
180     Atom a3 = b2.get1_2(ac);
181     AngleType angleType = forceField.getAngleType(a1.getAtomType(), ac.getAtomType(),
182         a3.getAtomType());
183     if (angleType == null) {
184       logNoAngleType(a1, ac, a3, forceField);
185       return null;
186     }
187     newAngle.setAngleType(angleType);
188 
189     return newAngle;
190   }
191 
192   /**
193    * {@inheritDoc}
194    */
195   @Override
196   public int compareTo(BondedTerm a) {
197     if (a == null) {
198       throw new NullPointerException();
199     }
200     if (a == this) {
201       return 0;
202     }
203     if (!a.getClass().isInstance(this)) {
204       return super.compareTo(a);
205     }
206     int this1 = atoms[1].getIndex();
207     int a1 = a.atoms[1].getIndex();
208     if (this1 < a1) {
209       return -1;
210     }
211     if (this1 > a1) {
212       return 1;
213     }
214 
215     int this0 = atoms[0].getIndex();
216     int a0 = a.atoms[0].getIndex();
217     if (this0 < a0) {
218       return -1;
219     }
220     if (this0 > a0) {
221       return 1;
222     }
223     int this2 = atoms[2].getIndex();
224     int a2 = a.atoms[2].getIndex();
225 
226     return Integer.compare(this2, a2);
227   }
228 
229   /**
230    * {@inheritDoc}
231    *
232    * <p>Evaluate this Angle energy.
233    */
234   @Override
235   public double energy(boolean gradient, int threadID, AtomicDoubleArray3D grad, AtomicDoubleArray3D lambdaGrad) {
236     value = 0.0;
237     energy = 0.0;
238     // Only compute this term if at least one atom is being used.
239     if (!getUse()) {
240       return energy;
241     }
242     var atomA = atoms[0];
243     var atomB = atoms[1];
244     var atomC = atoms[2];
245     var ia = atomA.getIndex() - 1;
246     var ib = atomB.getIndex() - 1;
247     var ic = atomC.getIndex() - 1;
248     var va = atomA.getXYZ();
249     var vb = atomB.getXYZ();
250     var vc = atomC.getXYZ();
251     var prefactor = angleType.angleUnit * rigidScale * angleType.forceConstant;
252     switch (angleType.angleMode) {
253       // Compute the bond angle bending energy.
254       case NORMAL -> {
255         var vab = va.sub(vb);
256         var vcb = vc.sub(vb);
257         var rab2 = vab.length2();
258         var rcb2 = vcb.length2();
259         if (rab2 != 0.0 && rcb2 != 0.0) {
260           var p = vcb.X(vab);
261           var cosine = min(1.0, max(-1.0, vab.dot(vcb) / sqrt(rab2 * rcb2)));
262           value = toDegrees(acos(cosine));
263           var dv = value - angleType.angle[nh];
264           var dv2 = dv * dv;
265           var dv3 = dv2 * dv;
266           var dv4 = dv2 * dv2;
267           energy = prefactor * dv2 * (1.0
268               + angleType.cubic * dv + angleType.quartic * dv2
269               + angleType.pentic * dv3 + angleType.sextic * dv4);
270           if (gradient) {
271             // Compute the bond angle bending gradient.
272             var deddt = prefactor * dv * toDegrees(2.0
273                 + 3.0 * angleType.cubic * dv + 4.0 * angleType.quartic * dv2
274                 + 5.0 * angleType.pentic * dv3 + 6.0 * angleType.sextic * dv4);
275             var rp = max(p.length(), 0.000001);
276             var terma = -deddt / (rab2 * rp);
277             var termc = deddt / (rcb2 * rp);
278             var ga = vab.X(p).scale(terma);
279             var gc = vcb.X(p).scale(termc);
280             grad.add(threadID, ia, ga);
281             grad.sub(threadID, ib, ga.add(gc));
282             grad.add(threadID, ic, gc);
283           }
284           value = dv;
285         }
286       }
287       case IN_PLANE -> {
288         // Compute the projected in-plane angle energy.
289         var vd = getAtom4XYZ();
290         int id = atom4.getIndex() - 1;
291         var vad = va.sub(vd);
292         var vbd = vb.sub(vd);
293         var vcd = vc.sub(vd);
294         var vp = vad.X(vcd);
295         var rp2 = vp.length2();
296         var delta = -vp.dot(vbd) / rp2;
297         var vip = vp.scale(delta).addI(vbd);
298         var vjp = vad.sub(vip);
299         var vkp = vcd.sub(vip);
300         var jp2 = vjp.length2();
301         var kp2 = vkp.length2();
302         if (jp2 != 0.0 && kp2 != 0.0) {
303           var cosine = min(1.0, max(-1.0, vjp.dot(vkp) / sqrt(jp2 * kp2)));
304           value = toDegrees(acos(cosine));
305           var dv = value - angleType.angle[nh];
306           var dv2 = dv * dv;
307           var dv3 = dv2 * dv;
308           var dv4 = dv2 * dv2;
309           energy = prefactor * dv2 * (1.0
310               + angleType.cubic * dv + angleType.quartic * dv2
311               + angleType.pentic * dv3 + angleType.sextic * dv4);
312           if (gradient) {
313             // Compute the projected in-plane angle gradient.
314             var deddt = prefactor * dv * toDegrees(2.0
315                 + 3.0 * angleType.cubic * dv + 4.0 * angleType.quartic * dv2
316                 + 5.0 * angleType.pentic * dv3 + 6.0 * angleType.sextic * dv4);
317             // Chain rule terms for first derivative components.
318             var lp = vkp.X(vjp);
319             var lpr = max(lp.length(), 0.000001);
320             var ded0 = vjp.X(lp).scaleI(-deddt / (jp2 * lpr));
321             var ded2 = vkp.X(lp).scaleI(deddt / (kp2 * lpr));
322             var dedp = ded0.add(ded2);
323             var gb = dedp.scale(-1.0);
324             var delta2 = 2.0 * delta;
325             var pt2 = dedp.dot(vp) / rp2;
326             var xd2 = vcd.X(gb).scaleI(delta);
327             var xp2 = vp.X(vcd).scaleI(delta2);
328             var x21 = vbd.X(vcd).addI(xp2).scaleI(pt2);
329             var dpd0 = xd2.add(x21);
330             xd2 = gb.X(vad).scaleI(delta);
331             xp2 = vp.X(vad).scaleI(delta2);
332             x21.addI(xp2).scaleI(pt2);
333             var dpd2 = xd2.addI(x21);
334             var ga = ded0.addI(dpd0);
335             var gc = ded2.addI(dpd2);
336             // Accumulate derivatives.
337             grad.add(threadID, ia, ga);
338             grad.add(threadID, ib, gb);
339             grad.add(threadID, ic, gc);
340             grad.sub(threadID, id, ga.addI(gb).addI(gc));
341           }
342           value = dv;
343         }
344       }
345     }
346 
347     return energy;
348   }
349 
350   /**
351    * If the specified atom is not the central atom of <b>this</b> angle, the atom of the opposite leg
352    * is returned. These atoms are said to be 1-3 to each other.
353    *
354    * @param a Atom
355    * @return Atom
356    */
357   public Atom get1_3(Atom a) {
358     if (a == atoms[0]) {
359       return atoms[2];
360     }
361     if (a == atoms[2]) {
362       return atoms[0];
363     }
364     return null;
365   }
366 
367   /**
368    * Getter for the field <code>angleMode</code>.
369    *
370    * @return a {@link ffx.potential.parameters.AngleType.AngleMode} object.
371    */
372   public AngleMode getAngleMode() {
373     return angleType.angleMode;
374   }
375 
376   /**
377    * Get the AngleType for this angle.
378    *
379    * @return This angle's AngleType.
380    */
381   public AngleType getAngleType() {
382     return angleType;
383   }
384 
385   /**
386    * Set a reference to the force field parameters for <b>this</b> Angle.
387    *
388    * @param a a {@link ffx.potential.parameters.AngleType} object.
389    */
390   public void setAngleType(AngleType a) {
391     angleType = a;
392 
393     // Count the number of hydrogen attached to the central atom, but that are not part of the
394     // angle.
395     List<Bond> ba = atoms[1].getBonds();
396     nh = 0;
397     for (Bond b1 : ba) {
398       if (b1 != bonds[0] && b1 != bonds[1]) {
399         Atom atom = b1.get1_2(atoms[1]);
400         if (atom.getAtomType().atomicNumber == 1) {
401           nh += 1;
402         }
403       }
404     }
405 
406     // Some angle bending parameters are generic for any number of hydrogen
407     while (angleType.angle.length <= nh) {
408       nh--;
409     }
410   }
411 
412   /**
413    * Getter for the field <code>atom4</code>.
414    *
415    * @return a {@link ffx.potential.bonded.Atom} object.
416    */
417   public Atom getAtom4() {
418     return atom4;
419   }
420 
421   /**
422    * getCentralAtom.
423    *
424    * @return a {@link ffx.potential.bonded.Atom} object.
425    */
426   public Atom getCentralAtom() {
427     return atoms[1];
428   }
429 
430   /**
431    * Log details for this Angle energy term.
432    */
433   public void log() {
434     switch (angleType.angleMode) {
435       case NORMAL -> logger.info(format(" %-8s %6d-%s %6d-%s %6d-%s %7.4f  %7.4f  %10.4f", "Angle",
436           atoms[0].getIndex(), atoms[0].getAtomType().name,
437           atoms[1].getIndex(), atoms[1].getAtomType().name,
438           atoms[2].getIndex(), atoms[2].getAtomType().name,
439           angleType.angle[nh], value, energy));
440       case IN_PLANE -> logger.info(format(" %-8s %6d-%s %6d-%s %6d-%s %7.4f  %7.4f  %10.4f", "Angle-IP",
441           atoms[0].getIndex(), atoms[0].getAtomType().name,
442           atoms[1].getIndex(), atoms[1].getAtomType().name,
443           atoms[2].getIndex(), atoms[2].getAtomType().name,
444           angleType.angle[nh], value, energy));
445     }
446   }
447 
448   @Override
449   public void setConstraint(Constraint c) {
450     super.setConstraint(c);
451     for (Bond b : bonds) {
452       b.setConstraint(c);
453     }
454   }
455 
456   /**
457    * Setter for the field <code>rigidScale</code>.
458    *
459    * @param rigidScale a double.
460    */
461   public void setRigidScale(double rigidScale) {
462     this.rigidScale = rigidScale;
463   }
464 
465   /**
466    * {@inheritDoc}
467    *
468    * <p>Overridden toString Method returns the Term's id.
469    */
470   @Override
471   public String toString() {
472     return format("%s  (%7.1f,%7.2f)", id, value, energy);
473   }
474 
475   /**
476    * Get the position of the 4th atom.
477    *
478    * @return Returns the position of the 4th atom, or throws an exception.
479    */
480   private Double3 getAtom4XYZ() {
481     try {
482       return atom4.getXYZ();
483     } catch (Exception e) {
484       logger.info(" Atom 4 not found for angle: " + this);
485       for (var atom : atoms) {
486         logger.info(" Atom: " + atom.toString());
487         logger.info(" Type: " + atom.getAtomType().toString());
488       }
489       throw e;
490     }
491   }
492 
493   /**
494    * Setter for the field <code>InPlaneAtom</code>.
495    *
496    * @param a4 a {@link ffx.potential.bonded.Atom} object.
497    */
498   void setInPlaneAtom(Atom a4) {
499     if (angleType.angleMode != AngleType.AngleMode.IN_PLANE) {
500       logger.severe(" Attempted to set fourth atom for a normal angle.");
501     }
502     atom4 = a4;
503   }
504 
505   /**
506    * Finds the common bond between <b>this</b> angle and another
507    *
508    * @param a An Angle that may have a common bond with <b>this</b> angle
509    * @return The common Bond between this Angle and Angle <b>a</b>, or null if this == a or no common
510    * bond exists.
511    */
512   Bond getCommonBond(Angle a) {
513     // Comparing an angle to itself returns null
514     // Comparing to a null angle return null
515     if (a == this || a == null) {
516       return null;
517     }
518     if (a.bonds[0] == bonds[0]) {
519       return bonds[0];
520     }
521     if (a.bonds[0] == bonds[1]) {
522       return bonds[1];
523     }
524     if (a.bonds[1] == bonds[0]) {
525       return bonds[0];
526     }
527     if (a.bonds[1] == bonds[1]) {
528       return bonds[1];
529     }
530     return null; // No common bond found
531   }
532 
533   /**
534    * Finds the other bond that makes up <b>this</b> angle
535    *
536    * @param b The bond to find the opposite of
537    * @return The other Bond that makes up this Angle, or null if Bond b is not part of this Angle
538    */
539   Bond getOtherBond(Bond b) {
540     if (b == bonds[0]) {
541       return bonds[1];
542     }
543     if (b == bonds[1]) {
544       return bonds[0];
545     }
546     return null; // b not found in angle
547   }
548 
549   /**
550    * If the central atom of the angle is trigonal, the 4th member of the trigonal center (that is not
551    * a part of the angle) will be returned.
552    *
553    * @return The 4th atom of a trigonal center.
554    */
555   public Atom getFourthAtomOfTrigonalCenter() {
556     if (atoms[1].isTrigonal()) {
557       for (Bond b : atoms[1].getBonds()) {
558         if (b != bonds[0] && b != bonds[1]) {
559           return b.get1_2(atoms[1]);
560         }
561       }
562     }
563     return null;
564   }
565 }