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.potential.MolecularAssembly;
41  import ffx.potential.parsers.PDBFilter;
42  import org.apache.commons.io.FilenameUtils;
43  
44  import java.io.File;
45  import java.util.ArrayList;
46  import java.util.Arrays;
47  import java.util.List;
48  import java.util.logging.Level;
49  import java.util.logging.Logger;
50  
51  import static java.lang.String.format;
52  import static java.lang.System.arraycopy;
53  import static org.apache.commons.math3.util.FastMath.abs;
54  import static org.apache.commons.math3.util.FastMath.exp;
55  import static org.apache.commons.math3.util.FastMath.max;
56  
57  /**
58   * SturmMethod class.
59   *
60   * @author Mallory R. Tollefson
61   * @since 1.0
62   */
63  public class SturmMethod {
64  
65    private static final Logger logger = Logger.getLogger(SturmMethod.class.getName());
66    final int MAXPOW = 32;
67    private final double RELERROR;
68    private final int MAXIT;
69    private final int MAX_ITER_SECANT;
70    private final double[][] xyz_o = new double[5][3];
71    private double[] roots;
72  
73    /** SturmMethod constructor with termination criteria for polynomial solver. */
74    public SturmMethod() {
75      RELERROR = 1.0e-15;
76      MAXIT = 100;
77      MAX_ITER_SECANT = 20;
78    }
79  
80    /**
81     * Calculates the modulus of u(x)/v(x) leaving it in r, it returns 0 if r(x) is constant. This
82     * function assumes the leading coefficient of v is 1 or -1.
83     *
84     * <p>Modp was originally a static int and returned r.ord as an int. The buildSturm function
85     * requires that a boolean is returned from the modp function, so modp is set to return a boolean.
86     * This new boolean return could be problematic elsewhere in the code if modp is used to return a
87     * value. This should be checked.
88     *
89     * @param u
90     * @param v
91     * @param r
92     * @return true if r.ord > 0.
93     */
94    private static boolean modp(Polynomial u, Polynomial v, Polynomial r) {
95      double[] nr = r.coefficients;
96      int end = u.order;
97      double[] uc = u.coefficients;
98  
99      if (end + 1 >= 0) arraycopy(uc, 0, nr, 0, end + 1);
100 
101     if (v.coefficients[v.order] < 0.0) {
102       for (int k = u.order - v.order - 1; k >= 0; k -= 2) {
103         r.coefficients[k] = -r.coefficients[k];
104       }
105 
106       for (int k = u.order - v.order; k >= 0; k--) {
107         for (int j = v.order + k - 1; j >= k; j--) {
108           r.coefficients[j] =
109               -r.coefficients[j] - r.coefficients[v.order + k] * v.coefficients[j - k];
110         }
111       }
112     } else {
113       for (int k = u.order - v.order; k >= 0; k--) {
114         for (int j = v.order + k - 1; j >= k; j--) {
115           r.coefficients[j] -= r.coefficients[v.order + k] * v.coefficients[j - k];
116         }
117       }
118     }
119 
120     int k = v.order - 1;
121     while (k >= 0 && abs(r.coefficients[k]) < 1.0e-18) {
122       r.coefficients[k] = 0.0;
123       k--;
124     }
125 
126     r.order = max(k, 0);
127 
128     if (r.order > 0) {
129       return true;
130     } else if (r.order == 0) {
131       return false;
132     } else {
133       return false;
134     }
135   }
136 
137   /**
138    * Used only in JUnit testing.
139    *
140    * @return oxygen coordinates.
141    */
142   public double[][] getr_o() {
143     return xyz_o;
144   }
145 
146   /**
147    * Solve using the Sturm method.
148    *
149    * @param order the order of the polynomial.
150    * @param poly_coeffs the coefficients of the polynomial.
151    * @param roots the array to hold the roots.
152    */
153   public int solveSturm(int order, double[] poly_coeffs, double[] roots) {
154     Polynomial[] sseq = new Polynomial[Polynomial.MAX_ORDER * 2];
155     double min, max;
156     int nroots, nchanges, np;
157     int[] atmin = new int[1];
158     int[] atmax = new int[1];
159     this.roots = roots;
160 
161     for (int i = 0; i < Polynomial.MAX_ORDER * 2; i++) {
162       sseq[i] = new Polynomial();
163     }
164 
165     if (order + 1 >= 0) arraycopy(poly_coeffs, 0, sseq[0].coefficients, 0, order + 1);
166 
167     if (logger.isLoggable(Level.FINE)) {
168       StringBuilder string = new StringBuilder();
169       for (int i = order; i >= 0; i--) {
170         string.append(" Coefficients in Sturm solver\n");
171         string.append(format("%d %f\n", i, sseq[0].coefficients[i]));
172       }
173       logger.fine(string.toString());
174     }
175 
176     np = buildSturm(order, sseq);
177 
178     if (logger.isLoggable(Level.FINE)) {
179       StringBuilder string1 = new StringBuilder();
180       string1.append(" Sturm sequence for:\n");
181       for (int i = order; i >= 0; i--) {
182         string1.append(format("%f ", sseq[0].coefficients[i]));
183         string1.append("\n");
184       }
185       for (int i = 0; i <= np; i++) {
186         for (int j = sseq[i].order; j >= 0; j--) {
187           string1.append(format("%f ", sseq[i].coefficients[j]));
188           string1.append("\n");
189         }
190       }
191       logger.fine(string1.toString());
192     }
193 
194     // get the number of real roots
195     nroots = numRoots(np, sseq, atmin, atmax);
196     if (logger.isLoggable(Level.FINE)) {
197       logger.fine(format(" Number of real roots: %d\n", nroots));
198     }
199 
200     // calculate the bracket that the roots live in
201     min = -1.0;
202     nchanges = numChanges(np, sseq, min);
203 
204     for (int i = 0; nchanges != atmin[0] && i != MAXPOW; i++) {
205       min *= 10.0;
206       nchanges = numChanges(np, sseq, min);
207     }
208 
209     if (nchanges != atmin[0]) {
210       logger.fine(" Solve: unable to bracket all negative roots\n");
211       atmin[0] = nchanges;
212     }
213 
214     max = 1.0;
215     nchanges = numChanges(np, sseq, max);
216 
217     for (int i = 0; nchanges != atmax[0] && i != MAXPOW; i++) {
218       max *= 10.0;
219       nchanges = numChanges(np, sseq, max);
220     }
221     if (nchanges != atmax[0]) {
222       logger.fine(" Solve: unable to bracket all positive roots\n");
223       atmax[0] = nchanges;
224     }
225 
226     // Perform the bisection
227     nroots = atmin[0] - atmax[0];
228     sturmBisection(np, sseq, min, max, atmin[0], atmax[0], this.roots);
229 
230     // write out the roots
231     if (logger.isLoggable(Level.FINE)) {
232       if (nroots == 1) {
233         logger.fine(format("\n One distinct real root at x = %f\n", this.roots[0]));
234       } else {
235         StringBuilder string2 = new StringBuilder();
236         string2.append(format("\n %d distinct real roots for x: \n", nroots));
237         for (int i = 0; i != nroots; i++) {
238           string2.append(format("%f\n", this.roots[i]));
239         }
240         logger.fine(string2.toString());
241       }
242     }
243 
244     return nroots;
245   }
246 
247   /**
248    * Write out loop coordinates and determine oxygen placement.
249    *
250    * @param r_n the coordinates of the nitrogen atoms.
251    * @param r_a the coordinates of the alpha carbon atoms.
252    * @param r_c the coordinates of the carbonyl carbon atoms.
253    * @param stt_res a int.
254    * @param end_res a int.
255    * @param molAss a {@link ffx.potential.MolecularAssembly} object.
256    * @param counter a int.
257    * @param writeFile a boolean.
258    * @return the File.
259    */
260   public File writePDBBackbone(
261       double[][] r_n,
262       double[][] r_a,
263       double[][] r_c,
264       int stt_res,
265       int end_res,
266       MolecularAssembly molAss,
267       int counter,
268       boolean writeFile) {
269 
270     Polymer[] newChain = molAss.getChains();
271     List<Atom> backBoneAtoms;
272     double[] xyz_n = new double[3];
273     double[] xyz_a = new double[3];
274     double[] xyz_c = new double[3];
275 
276     xyz_o[0][0] = 0.0;
277     xyz_o[0][1] = 0.0;
278     xyz_o[0][2] = 0.0;
279     xyz_o[4][0] = 0.0;
280     xyz_o[4][1] = 0.0;
281     xyz_o[4][2] = 0.0;
282 
283     List<Atom> OAtoms = new ArrayList<>();
284 
285     for (int i = stt_res + 1; i < end_res; i++) {
286       Residue newResidue = newChain[0].getResidue(i);
287       backBoneAtoms = newResidue.getBackboneAtoms();
288       for (Atom backBoneAtom : backBoneAtoms) {
289         switch (backBoneAtom.getAtomType().name) {
290           case "C":
291             xyz_c[0] = r_c[i - stt_res][0];
292             xyz_c[1] = r_c[i - stt_res][1];
293             xyz_c[2] = r_c[i - stt_res][2];
294             backBoneAtom.moveTo(xyz_c);
295             break;
296           case "N":
297             xyz_n[0] = r_n[i - stt_res][0];
298             xyz_n[1] = r_n[i - stt_res][1];
299             xyz_n[2] = r_n[i - stt_res][2];
300             backBoneAtom.moveTo(xyz_n);
301             break;
302           case "CA":
303             xyz_a[0] = r_a[i - stt_res][0];
304             xyz_a[1] = r_a[i - stt_res][1];
305             xyz_a[2] = r_a[i - stt_res][2];
306             backBoneAtom.moveTo(xyz_a);
307             break;
308           case "O":
309             OAtoms.add(backBoneAtom);
310             break;
311           default:
312             newResidue.deleteAtom(backBoneAtom);
313             break;
314         }
315       }
316 
317       List<Atom> sideChainAtoms = newResidue.getSideChainAtoms();
318       for (Atom sideChainAtom : sideChainAtoms) {
319         newResidue.deleteAtom(sideChainAtom);
320       }
321     }
322 
323     int oCount = 0;
324     for (int i = stt_res + 1; i < end_res; i++) {
325       Residue newResidue = newChain[0].getResidue(i);
326       backBoneAtoms = newResidue.getBackboneAtoms();
327       Atom CA = new Atom("CA");
328       Atom N = new Atom("N");
329       Atom C = new Atom("C");
330       Atom O = OAtoms.get(oCount);
331 
332       for (Atom backBoneAtom : backBoneAtoms) {
333         switch (backBoneAtom.getAtomType().name) {
334           case "C":
335             C = backBoneAtom;
336             break;
337           case "N":
338             N = backBoneAtom;
339             break;
340           case "CA":
341             CA = backBoneAtom;
342             break;
343           default:
344             break;
345         }
346       }
347       BondedUtils.intxyz(O, C, 1.2255, CA, 122.4, N, 180, 0);
348       xyz_o[i - stt_res][0] = O.getX();
349       xyz_o[i - stt_res][1] = O.getY();
350       xyz_o[i - stt_res][2] = O.getZ();
351 
352       oCount++;
353     }
354 
355     File file = molAss.getFile();
356 
357     String filename = FilenameUtils.removeExtension(file.getAbsolutePath());
358     if (!filename.contains("_loop")) {
359       filename = filename + "_loop";
360     }
361 
362     File modifiedFile = new File(filename + ".pdb_" + counter);
363     PDBFilter modFilter = new PDBFilter(modifiedFile, molAss, null, null);
364 
365     if (writeFile) {
366       modFilter.writeFile(modifiedFile, true);
367     }
368 
369     return (modifiedFile);
370   }
371 
372   /**
373    * The hyperTan method.
374    *
375    * @param a
376    * @param x
377    * @return 1.0 if (ax > 100), -1.0 if (ax < -100) and (exp(ax)-exp(-ax))/(exp(ax)+exp(-ax))
378    *     otherwise.
379    */
380   private double hyperTan(double a, double x) {
381     double ax = a * x;
382     if (ax > 100.0) {
383       return 1.0;
384     } else if (ax < -100.0) {
385       return -1.0;
386     } else {
387       double exp_x1 = exp(ax);
388       double exp_x2 = exp(-ax);
389       return (exp_x1 - exp_x2) / (exp_x1 + exp_x2);
390     }
391   }
392 
393   /**
394    * Build up a sturm sequence for a polynomial, and return the number of polynomials in the
395    * sequence.
396    *
397    * @param ord
398    * @param sseq
399    * @return the number of polynomials in the sequence.
400    */
401   private int buildSturm(int ord, Polynomial[] sseq) {
402     double f;
403     double[] fp;
404     double[] fc;
405 
406     sseq[0].order = ord;
407     sseq[1].order = ord - 1;
408 
409     // Calculate the derivative and normalise the leading coefficient
410     f = abs(sseq[0].coefficients[ord] * ord);
411     fp = sseq[1].coefficients;
412     fc = Arrays.copyOfRange(sseq[0].coefficients, 1, sseq[0].coefficients.length);
413 
414     int j = 0;
415     for (int i = 1; i <= ord; i++) {
416       fp[j] = fc[j] * i / f;
417       j++;
418     }
419 
420     // Construct the rest of the Sturm sequence. (Double check this... )
421     int i;
422     for (i = 0;
423         i < sseq[0].coefficients.length - 2 && modp(sseq[i], sseq[i + 1], sseq[i + 2]);
424         i++) {
425       // reverse the sign and normalise
426       f = -Math.abs(sseq[i + 2].coefficients[sseq[i + 2].order]);
427       for (j = sseq[i + 2].order; j >= 0; j--) {
428         sseq[i + 2].coefficients[j] /= f;
429       }
430     }
431 
432     // Reverse the sign.
433     sseq[i + 2].coefficients[0] = -sseq[i + 2].coefficients[0];
434 
435     return (sseq[0].order - sseq[i + 2].order);
436   }
437 
438   /**
439    * Return the number of distinct real roots of the polynomial described in sseq.
440    *
441    * @param np
442    * @param sseq
443    * @param atneg
444    * @param atpos
445    * @return the number of distinct real roots of sseq.
446    */
447   private int numRoots(int np, Polynomial[] sseq, int[] atneg, int[] atpos) {
448 
449     int atposinf = 0;
450     int atneginf = 0;
451 
452     // Changes at positive infinity.
453     double lf = sseq[0].coefficients[sseq[0].order];
454 
455     for (int i = 1; i <= np; i++) // for (s = sseq + 1; s <= sseq + np; s++)
456     {
457       double f = sseq[i].coefficients[sseq[i].order];
458       if (lf == 0.0 || lf * f < 0) {
459         atposinf++;
460       }
461       lf = f;
462     }
463 
464     // Changes at negative infinity.
465     if ((sseq[0].order & 1) != 0) {
466       lf = -sseq[0].coefficients[sseq[0].order];
467     } else {
468       lf = sseq[0].coefficients[sseq[0].order];
469     }
470 
471     for (int i = 1; i <= np; i++) {
472       double f;
473       if ((sseq[i].order & 1) != 0) {
474         f = -sseq[i].coefficients[sseq[i].order];
475       } else {
476         f = sseq[i].coefficients[sseq[i].order];
477       }
478       if (lf == 0.0 || lf * f < 0) {
479         atneginf++;
480       }
481       lf = f;
482     }
483 
484     atneg[0] = atneginf;
485     atpos[0] = atposinf;
486 
487     return (atneginf - atposinf);
488   }
489 
490   /**
491    * Return the number of sign changes in the Sturm sequence in sseq at the value a.
492    *
493    * @param np
494    * @param sseq
495    * @param a
496    * @return the number of sign changes.
497    */
498   private int numChanges(int np, Polynomial[] sseq, double a) {
499 
500     int changes = 0;
501     double lf = evalPoly(sseq[0].order, sseq[0].coefficients, a);
502     for (int i = 1; i <= np; i++) {
503       double f = evalPoly(sseq[i].order, sseq[i].coefficients, a);
504       if (lf == 0.0 || lf * f < 0) {
505         changes++;
506       }
507 
508       lf = f;
509     }
510 
511     return changes;
512   }
513 
514   /**
515    * Uses a bisection based on the Sturm sequence for the polynomial described in sseq to isolate
516    * intervals in which roots occur, the roots are returned in the roots array in order of
517    * magnitude.
518    *
519    * @param np
520    * @param sseq
521    * @param min
522    * @param max
523    * @param atmin
524    * @param atmax
525    * @param roots
526    */
527   private void sturmBisection(
528       int np, Polynomial[] sseq, double min, double max, int atmin, int atmax, double[] roots) {
529     double mid = 0;
530     int n1 = 0;
531     int n2 = 0;
532     int its, atmid, nroot;
533 
534     nroot = atmin - atmax;
535     if (nroot == 1) {
536       // First try a less expensive technique
537       if (modifiedRegulaFalsi(sseq[0].order, sseq[0].coefficients, min, max, roots)) {
538         fillArray(roots, this.roots);
539         return;
540       }
541 
542       // When code reaches this point, the root must be evaluated using the Sturm sequence.
543       for (its = 0; its < MAXIT; its++) {
544         mid = (min + max) / 2;
545         atmid = numChanges(np, sseq, mid);
546         if (abs(mid) > RELERROR) {
547           if (abs((max - min) / mid) < RELERROR) {
548             roots[0] = mid;
549             fillArray(roots, this.roots);
550             return;
551           }
552         } else if (abs(max - min) < RELERROR) {
553           roots[0] = mid;
554           fillArray(roots, this.roots);
555           return;
556         }
557         if ((atmin - atmid) == 0) {
558           min = mid;
559         } else {
560           max = mid;
561         }
562       }
563       if (its == MAXIT) {
564         logger.info(
565             format(
566                 " sbisect: overflow min %f max %f diff %f nroot %d n1 %d n2 %d\n",
567                 min, max, max - min, nroot, n1, n2));
568         roots[0] = mid;
569       }
570       fillArray(roots, this.roots);
571       return;
572     }
573 
574     // More than one root in the interval, must bisect.
575     for (its = 0; its < MAXIT; its++) {
576       mid = (min + max) / 2;
577       atmid = numChanges(np, sseq, mid);
578       n1 = atmin - atmid;
579       n2 = atmid - atmax;
580       if (n1 != 0 && n2 != 0) {
581         sturmBisection(np, sseq, min, mid, atmin, atmid, roots);
582         // Keep a reference to this array so that its values can be copied into the
583         // roots array later.
584         final double[] rootsSection = Arrays.copyOfRange(roots, n1, roots.length);
585         sturmBisection(np, sseq, mid, max, atmid, atmax, rootsSection);
586         fillArray(rootsSection, roots);
587         break;
588       }
589       if (n1 == 0) {
590         min = mid;
591       } else {
592         max = mid;
593       }
594     }
595 
596     if (its == MAXIT) {
597       for (n1 = atmax; n1 < atmin; n1++) {
598         roots[n1 - atmax] = mid;
599       }
600     }
601   }
602 
603   /**
604    * This method fills the target array with values from the values array. The context of this
605    * method makes the most sense when the values array is smaller than the target array, resulting
606    * in the data from the values array simply being added to the target array at the first point
607    * that would entail the final value being copied into the target array's final index. See below
608    * for an example input and output.
609    *
610    * <pre>
611    * Inputs:
612    * valuesArray: [ 10, 20, 30,  0 ]
613    * targetArray: [  1,  2,  3,  4,  0,  0,  0,  0,  0,  0];
614    *
615    * Output (via array reference with later use of target array):
616    * [  1,  2,  3,  4,  0,  0, 10, 20, 30, 0 ]
617    * </pre>
618    *
619    * @param valuesArray The array that will hold the values to copy into the target array.
620    * @param targetArray The array that will receive the values in the values array.
621    */
622   private void fillArray(final double[] valuesArray, final double[] targetArray) {
623     final int tarLen = targetArray.length;
624     final int valLen = valuesArray.length;
625     if (tarLen >= valLen) {
626       for (int tarIndex = tarLen - valLen, valIndex = 0;
627           tarIndex < tarLen;
628           tarIndex++, valIndex++) {
629         targetArray[tarIndex] = valuesArray[valIndex];
630       }
631     } else {
632       logger.info(" sbisect: length of values array is too long for the target array; not filling");
633     }
634   }
635 
636   /**
637    * Evaluate polynomial defined in coef returning its value.
638    *
639    * @param ord
640    * @param coef
641    * @param x
642    * @return value of the polynomial at x.
643    */
644   private double evalPoly(int ord, double[] coef, double x) {
645 
646     double[] fp = coef;
647     double f = fp[ord];
648 
649     int i = ord;
650     for (i--; i >= 0; i--) {
651       f = x * f + fp[i];
652     }
653     return f;
654   }
655 
656   /**
657    * Uses the modified regula-falsi method to evaluate the root in interval [a,b] of the polynomial
658    * described in coef. The root is returned in val. The routine returns zero if it can't converge.
659    *
660    * @param ord
661    * @param coef
662    * @param a
663    * @param b
664    * @param val
665    * @return 0 if the method does not converge.
666    */
667   private boolean modifiedRegulaFalsi(int ord, double[] coef, double a, double b, double[] val) {
668     double fa = coef[ord];
669     double fb = fa;
670 
671     for (int i = ord - 1; i >= 0; i--) {
672       fa = a * fa + coef[i];
673       fb = b * fb + coef[i];
674     }
675     if (fa * fb > 0.0) {
676       return false;
677     }
678     double lfx = fa;
679     for (int its = 0; its < MAX_ITER_SECANT; its++) {
680       double x = (fb * a - fa * b) / (fb - fa);
681       // constrain that x stays in the bounds
682       if (x < a || x > b) {
683         x = 0.5 * (a + b);
684       }
685       double fx = coef[ord];
686       for (int i = ord - 1; i >= 0; i--) {
687         fx = x * fx + coef[i];
688       }
689       if (abs(x) > RELERROR) {
690         if (abs(fx / x) < RELERROR) {
691           val[0] = x;
692           return true;
693         }
694       } else if (abs(fx) < RELERROR) {
695         val[0] = x;
696         return true;
697       }
698       if ((fa * fx) < 0) {
699         b = x;
700         fb = fx;
701         if ((lfx * fx) > 0) {
702           fa /= 2;
703         }
704       } else {
705         a = x;
706         fa = fx;
707         if ((lfx * fx) > 0) {
708           fb /= 2;
709         }
710       }
711 
712       lfx = fx;
713     }
714 
715     return false;
716   }
717 
718   /** The Polynomial class describes a single polynomial of up to 16th degree. */
719   private static class Polynomial {
720 
721     static final int MAX_ORDER = 16;
722     /** The coefficients of the polynomial. */
723     public final double[] coefficients;
724     /** The order of the polynomial. */
725     public int order;
726 
727     /** Private constructor. */
728     private Polynomial() {
729       coefficients = new double[MAX_ORDER + 1];
730     }
731   }
732 }