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.bonded;
39  
40  import ffx.numerics.atomic.AtomicDoubleArray3D;
41  import ffx.numerics.math.DoubleMath;
42  import ffx.potential.parameters.AtomType;
43  import ffx.potential.parameters.ForceField;
44  import ffx.potential.parameters.TorsionType;
45  import org.apache.commons.math3.util.FastMath;
46  
47  import java.io.Serial;
48  import java.util.ArrayList;
49  import java.util.List;
50  import java.util.function.DoubleUnaryOperator;
51  import java.util.logging.Logger;
52  
53  import static ffx.numerics.math.DoubleMath.dihedralAngle;
54  import static java.lang.String.format;
55  import static org.apache.commons.math3.util.FastMath.acos;
56  import static org.apache.commons.math3.util.FastMath.max;
57  import static org.apache.commons.math3.util.FastMath.sqrt;
58  import static org.apache.commons.math3.util.FastMath.toDegrees;
59  
60  /**
61   * The Torsion class represents a torsional angle formed between four bonded atoms.
62   *
63   * @author Michael J. Schnieders
64   * @since 1.0
65   */
66  public class Torsion extends BondedTerm implements LambdaInterface {
67  
68    @Serial
69    private static final long serialVersionUID = 1L;
70  
71    private static final Logger logger = Logger.getLogger(Torsion.class.getName());
72    /**
73     * The force field Torsion type in use.
74     */
75    public TorsionType torsionType = null;
76    /**
77     * Scale up the torsional energy by this factor.
78     */
79    private double torsionScale = 1.0;
80    /**
81     * Value of lambda.
82     */
83    private double lambda = 1.0;
84    /**
85     * Value of dE/dL.
86     */
87    private double dEdL = 0.0;
88    /**
89     * Flag to indicate lambda dependence.
90     */
91    private boolean lambdaTerm = false;
92    /**
93     * Maps global lambda to either itself or 1 - global lambda.
94     */
95    private final DoubleUnaryOperator lambdaMapper;
96    /**
97     * Set the tolerance for minimum distance and angle values.
98     */
99    private static final double TORSION_TOLERANCE = 1.0e-4;
100 
101   /**
102    * Create a Torsion from 3 connected bonds (no error checking)
103    *
104    * @param b1 The first Bond.
105    * @param b2 The middle Bond.
106    * @param b3 The last Bond.
107    */
108   public Torsion(Bond b1, Bond b2, Bond b3) {
109     super();
110     bonds = new Bond[3];
111     bonds[0] = b1;
112     bonds[1] = b2;
113     bonds[2] = b3;
114     lambdaMapper = (double d) -> d;
115     initialize();
116   }
117 
118   /**
119    * Log that no TorsionType exists.
120    *
121    * @param a0 Atom 0.
122    * @param a1 Atom 1.
123    * @param a2 Atom 2.
124    * @param a3 Atom 3.
125    */
126   public static void logNoTorsionType(Atom a0, Atom a1, Atom a2, Atom a3, ForceField forceField) {
127     AtomType atomType0 = a0.getAtomType();
128     AtomType atomType1 = a1.getAtomType();
129     AtomType atomType2 = a2.getAtomType();
130     AtomType atomType3 = a3.getAtomType();
131     int[] c = {atomType0.atomClass, atomType1.atomClass, atomType2.atomClass, atomType3.atomClass};
132     String key = TorsionType.sortKey(c);
133     StringBuilder sb = new StringBuilder(
134         format("No TorsionType for key: %s\n %s -> %s\n %s -> %s\n %s -> %s\n %s -> %s", key, a0,
135             atomType0, a1, atomType1, a2, atomType2, a3, atomType3));
136     if (matchTorsions(a0, a1, a2, a3, forceField, sb, true) <= 0) {
137       matchTorsions(a0, a1, a2, a3, forceField, sb, false);
138     }
139     logger.severe(sb.toString());
140   }
141 
142   private static int matchTorsions(Atom a0, Atom a1, Atom a2, Atom a3, ForceField forceField,
143                                    StringBuilder sb, boolean strict) {
144     AtomType atomType0 = a0.getAtomType();
145     AtomType atomType1 = a1.getAtomType();
146     AtomType atomType2 = a2.getAtomType();
147     AtomType atomType3 = a3.getAtomType();
148     int c0 = atomType0.atomClass;
149     int c1 = atomType1.atomClass;
150     int c2 = atomType2.atomClass;
151     int c3 = atomType3.atomClass;
152     List<AtomType> types0 = forceField.getSimilarAtomTypes(atomType0);
153     List<AtomType> types1 = forceField.getSimilarAtomTypes(atomType1);
154     List<AtomType> types2 = forceField.getSimilarAtomTypes(atomType2);
155     List<AtomType> types3 = forceField.getSimilarAtomTypes(atomType3);
156     List<TorsionType> torsionTypes = new ArrayList<>();
157     boolean match = false;
158     for (AtomType type1 : types1) {
159       for (AtomType type2 : types2) {
160         // Match both inner atom classes.
161         if ((type1.atomClass == c1 && type2.atomClass == c2) || (type1.atomClass == c2 && type2.atomClass == c1)) {
162           for (AtomType type0 : types0) {
163             for (AtomType type3 : types3) {
164               // Match one distal atom class.
165               if (strict) {
166                 if ((type0.atomClass != c0) && (type0.atomClass != c3) && (type3.atomClass != c0) && (type3.atomClass != c3)) {
167                   continue;
168                 }
169               }
170               TorsionType torsionType = forceField.getTorsionType(type0, type1, type2, type3);
171               if (torsionType != null && !torsionTypes.contains(torsionType)) {
172                 if (!match) {
173                   match = true;
174                   sb.append("\n Similar Torsion Types:");
175                 }
176                 torsionTypes.add(torsionType);
177                 sb.append(format("\n  %s", torsionType));
178               }
179             }
180           }
181         }
182       }
183     }
184     return torsionTypes.size();
185   }
186 
187   /**
188    * Attempt to create a new Torsion based on the supplied bonds. There is no error checking to
189    * enforce that the bonds make up a linear series of 4 bonded atoms.
190    *
191    * @param bond1      the first Bond.
192    * @param middleBond the middle Bond.
193    * @param bond3      the last Bond.
194    * @param forceField the ForceField parameters to apply.
195    * @return a new Torsion, or null.
196    */
197   static Torsion torsionFactory(Bond bond1, Bond middleBond, Bond bond3, ForceField forceField) {
198     Atom a0 = bond1.getOtherAtom(middleBond);
199     Atom a1 = middleBond.getAtom(0);
200     Atom a2 = middleBond.getAtom(1);
201     Atom a3 = bond3.getOtherAtom(middleBond);
202 
203     TorsionType torsionType = forceField.getTorsionType(a0.getAtomType(), a1.getAtomType(),
204         a2.getAtomType(), a3.getAtomType());
205 
206     // No torsion type found.
207     if (torsionType == null) {
208       logNoTorsionType(a0, a1, a2, a3, forceField);
209       return null;
210     }
211 
212     Torsion torsion = new Torsion(bond1, middleBond, bond3);
213     torsion.setTorsionType(torsionType);
214     // Scale-up torsions that do not include hydrogen or halogen atoms.
215     if (forceField.hasProperty("torsion-scale")) {
216       // Check the identity of the terminal atoms.
217       boolean isTerminal = (a0.isHydrogen() || a0.isHalogen() || a3.isHydrogen() || a3.isHalogen());
218       if (!isTerminal) {
219         double scale = forceField.getDouble("torsion-scale", 1.0);
220         torsion.setTorsionScale(scale);
221       }
222     }
223 
224     return torsion;
225   }
226 
227   /**
228    * Set the torsion type.
229    *
230    * @param torsionType The TorsionType.
231    */
232   public void setTorsionType(TorsionType torsionType) {
233     this.torsionType = torsionType;
234   }
235 
236   /**
237    * Set the torsion scale up factor.
238    *
239    * @param torsionScale The torsion scale up factor.
240    */
241   public void setTorsionScale(double torsionScale) {
242     this.torsionScale = torsionScale;
243   }
244 
245   /**
246    * Get the torsion scale up factor.
247    *
248    * @return The torsion scale up factor.
249    */
250   public double getTorsionScale() {
251     return torsionScale;
252   }
253 
254   /**
255    * compare
256    *
257    * @param a0 a {@link ffx.potential.bonded.Atom} object.
258    * @param a1 a {@link ffx.potential.bonded.Atom} object.
259    * @param a2 a {@link ffx.potential.bonded.Atom} object.
260    * @param a3 a {@link ffx.potential.bonded.Atom} object.
261    * @return a boolean.
262    */
263   public boolean compare(Atom a0, Atom a1, Atom a2, Atom a3) {
264     if (a0 == atoms[0] && a1 == atoms[1] && a2 == atoms[2] && a3 == atoms[3]) {
265       return true;
266     }
267     return (a0 == atoms[3] && a1 == atoms[2] && a2 == atoms[1] && a3 == atoms[0]);
268   }
269 
270   /**
271    * Compute the torsional angle in degrees.
272    *
273    * @return The torsion in degrees.
274    */
275   public double measure() {
276     double angle = DoubleMath.dihedralAngle(atoms[0].getXYZ(null), atoms[1].getXYZ(null),
277         atoms[2].getXYZ(null), atoms[3].getXYZ(null));
278     return toDegrees(angle);
279   }
280 
281   /**
282    * {@inheritDoc}
283    *
284    * <p>Evaluate the Torsional Angle energy.
285    */
286   @Override
287   public double energy(boolean gradient, int threadID, AtomicDoubleArray3D grad, AtomicDoubleArray3D lambdaGrad) {
288     energy = 0.0;
289     value = 0.0;
290     dEdL = 0.0;
291     // Only compute this term if at least one atom is being used.
292     if (!getUse()) {
293       return energy;
294     }
295     var atomA = atoms[0];
296     var atomB = atoms[1];
297     var atomC = atoms[2];
298     var atomD = atoms[3];
299     // Compute the value of the torsional angle.
300     var va = atomA.getXYZ();
301     var vb = atomB.getXYZ();
302     var vc = atomC.getXYZ();
303     var vd = atomD.getXYZ();
304     var vba = vb.sub(va);
305     var vcb = vc.sub(vb);
306     var vdc = vd.sub(vc);
307     var vt = vba.X(vcb);
308     var vu = vcb.X(vdc);
309     var rt2 = max(vt.length2(), TORSION_TOLERANCE);
310     var ru2 = max(vu.length2(), TORSION_TOLERANCE);
311     var rtru2 = rt2 * ru2;
312     var rr = sqrt(rtru2);
313     var rcb = max(vcb.length(), TORSION_TOLERANCE);
314     var cosine = vt.dot(vu) / rr;
315     var sine = vcb.dot(vt.X(vu)) / (rcb * rr);
316     value = toDegrees(acos(cosine));
317     if (sine < 0.0) {
318       value = -value;
319     }
320 
321     // Calculate the torsional energy for this angle.
322     var amp = torsionType.amplitude;
323     var tsin = torsionType.sine;
324     var tcos = torsionType.cosine;
325 
326     // Add the energy for the first term.
327     energy = amp[0] * (1.0 + cosine * tcos[0] + sine * tsin[0]);
328     var dedphi = amp[0] * (cosine * tsin[0] - sine * tcos[0]);
329     var cosprev = cosine;
330     var sinprev = sine;
331     var n = torsionType.terms;
332 
333     // Add the energy for the remaining terms.
334     for (int i = 1; i < n; i++) {
335       var cosn = cosine * cosprev - sine * sinprev;
336       var sinn = sine * cosprev + cosine * sinprev;
337       var phi = 1.0 + cosn * tcos[i] + sinn * tsin[i];
338       var dphi = (1.0 + i) * (cosn * tsin[i] - sinn * tcos[i]);
339       energy = energy + amp[i] * phi;
340       dedphi = dedphi + amp[i] * dphi;
341       cosprev = cosn;
342       sinprev = sinn;
343     }
344 
345     dEdL = torsionScale * torsionType.torsionUnit * energy;
346     energy = dEdL * lambda;
347 
348     // Compute the torsional gradient for this angle.
349     if (gradient || lambdaTerm) {
350       // Chain rule terms for first derivative components.
351       dedphi = torsionScale * torsionType.torsionUnit * dedphi;
352       var vca = vc.sub(va);
353       var vdb = vd.sub(vb);
354       var dedt = vt.X(vcb).scaleI(dedphi / (rt2 * rcb));
355       var dedu = vu.X(vcb).scaleI(-dedphi / (ru2 * rcb));
356 
357       // Compute first derivative components for this angle.
358       var ga = dedt.X(vcb);
359       var gb = vca.X(dedt).addI(dedu.X(vdc));
360       var gc = dedt.X(vba).addI(vdb.X(dedu));
361       var gd = dedu.X(vcb);
362       int ia = atomA.getIndex() - 1;
363       int ib = atomB.getIndex() - 1;
364       int ic = atomC.getIndex() - 1;
365       int id = atomD.getIndex() - 1;
366       if (lambdaTerm) {
367         lambdaGrad.add(threadID, ia, ga);
368         lambdaGrad.add(threadID, ib, gb);
369         lambdaGrad.add(threadID, ic, gc);
370         lambdaGrad.add(threadID, id, gd);
371       }
372       if (gradient) {
373         grad.add(threadID, ia, ga.scaleI(lambda));
374         grad.add(threadID, ib, gb.scaleI(lambda));
375         grad.add(threadID, ic, gc.scaleI(lambda));
376         grad.add(threadID, id, gd.scaleI(lambda));
377       }
378     }
379 
380     return energy;
381   }
382 
383   /**
384    * If the specified atom is not a central atom of <b>this</b> torsion, the atom at the opposite end
385    * is returned. These atoms are said to be 1-4 to each other.
386    *
387    * @param a Atom
388    * @return Atom
389    */
390   public Atom get1_4(Atom a) {
391     if (a == atoms[0]) {
392       return atoms[3];
393     }
394     if (a == atoms[3]) {
395       return atoms[0];
396     }
397     return null;
398   }
399 
400   /**
401    * {@inheritDoc}
402    */
403   @Override
404   public double getLambda() {
405     return lambda;
406   }
407 
408   /**
409    * {@inheritDoc}
410    */
411   @Override
412   public void setLambda(double lambda) {
413     if (applyAllLambda()) {
414       lambdaTerm = true;
415     }
416     this.lambda = lambdaTerm ? lambdaMapper.applyAsDouble(lambda) : 1.0;
417   }
418 
419   /**
420    * {@inheritDoc}
421    */
422   @Override
423   public double getd2EdL2() {
424     return 0.0;
425   }
426 
427   /**
428    * {@inheritDoc}
429    */
430   @Override
431   public double getdEdL() {
432     if (lambdaTerm) {
433       return dEdL;
434     } else {
435       return 0.0;
436     }
437   }
438 
439   /**
440    * {@inheritDoc}
441    */
442   @Override
443   public void getdEdXdL(double[] gradient) {
444     // The chain rule term is zero.
445   }
446 
447   /**
448    * Log details for this Torsional Angle energy term.
449    */
450   public void log() {
451     logger.info(format(" %-8s %6d-%s %6d-%s %6d-%s %6d-%s %10.4f %10.4f %10.4f", "Torsional-Angle",
452         atoms[0].getIndex(), atoms[0].getAtomType().name, atoms[1].getIndex(),
453         atoms[1].getAtomType().name, atoms[2].getIndex(), atoms[2].getAtomType().name,
454         atoms[3].getIndex(), atoms[3].getAtomType().name, value, energy, torsionScale));
455   }
456 
457   /**
458    * {@inheritDoc}
459    *
460    * <p>Overridden toString Method returns the Term's id.
461    */
462   @Override
463   public String toString() {
464     return format("%s  (%7.1f,%7.2f,%7.2f)", id, value, energy, torsionScale);
465   }
466 
467   /**
468    * Initialization
469    */
470   private void initialize() {
471     atoms = new Atom[4];
472     atoms[1] = bonds[0].getCommonAtom(bonds[1]);
473     atoms[0] = bonds[0].get1_2(atoms[1]);
474     atoms[2] = bonds[1].get1_2(atoms[1]);
475     atoms[3] = bonds[2].get1_2(atoms[2]);
476     atoms[0].setTorsion(this);
477     atoms[1].setTorsion(this);
478     atoms[2].setTorsion(this);
479     atoms[3].setTorsion(this);
480     setID_Key(false);
481     value = dihedralAngle(atoms[0].getXYZ(null), atoms[1].getXYZ(null), atoms[2].getXYZ(null),
482         atoms[3].getXYZ(null));
483   }
484 }