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