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