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.constraint;
39  
40  import ffx.numerics.Constraint;
41  import ffx.potential.bonded.Angle;
42  import ffx.potential.bonded.Atom;
43  import ffx.potential.bonded.Bond;
44  import java.util.ArrayList;
45  import java.util.Arrays;
46  import java.util.HashSet;
47  import java.util.List;
48  import java.util.Set;
49  import java.util.logging.Logger;
50  import java.util.stream.IntStream;
51  import org.apache.commons.math3.linear.OpenMapRealMatrix;
52  import org.apache.commons.math3.linear.QRDecomposition;
53  import org.apache.commons.math3.linear.RealMatrix;
54  import org.apache.commons.math3.linear.RealVectorPreservingVisitor;
55  import org.apache.commons.math3.util.FastMath;
56  
57  public class CcmaConstraint implements Constraint {
58  
59    public static final double DEFAULT_CCMA_NONZERO_CUTOFF = 0.01;
60    private static final Logger logger = Logger.getLogger(CcmaConstraint.class.getName());
61    private static final int DEFAULT_MAX_ITERS = 150;
62    // Might be slightly more elegant to have "component constraint" objects.
63    // This is how OpenMM does it, though, and arrays are likely more performative.
64    private final int[] atoms1;
65    private final int[] atoms2;
66    private final double[] lengths;
67    private final int nConstraints;
68    private final int[] uniqueIndices;
69    private final int maxIters = DEFAULT_MAX_ITERS;
70    private final double elementCutoff = DEFAULT_CCMA_NONZERO_CUTOFF;
71    private final double[] reducedMasses;
72    private final RealMatrix kInvSparse;
73  
74    /**
75     * Constructs a set of bond length Constraints to be satisfied using the Constaint Constraint
76     * Matrix Approximation, a parallelizable stable numeric method.
77     *
78     * <p>The nonzeroCutoff field specifies how large an element of K-1 needs to be to be kept;
79     * smaller elements (indicating weak constraint-constraint couplings) are set to zero to make K-1
80     * sparse and thus keep CCMA tractable.
81     *
82     * <p>Due to leaking-this issues, a thin Factory method is provided to build this.
83     *
84     * <p>The constrainedAngles will constrain both component bonds and the triangle-closing distance.
85     * The constrainedBonds list should not include any bonds included in the constrained angles.
86     *
87     * @param constrainedBonds Bonds to be constrained, not included in constrainedAngles
88     * @param constrainedAngles Angles to be constrained indirectly (by constructing a rigid
89     *     triangle).
90     * @param allAtoms All Atoms of the system, including unconstrained Atoms.
91     * @param masses All masses of the system, including unconstrained atom masses.
92     * @param nonzeroCutoff CCMA parameter defining how sparse/dense K-1 should be.
93     */
94    private CcmaConstraint(
95        List<Bond> constrainedBonds,
96        List<Angle> constrainedAngles,
97        final Atom[] allAtoms,
98        final double[] masses,
99        double nonzeroCutoff) {
100 
101     long time = -System.nanoTime();
102     int nBonds = constrainedBonds.size();
103     int nAngles = constrainedAngles.size();
104     assert constrainedAngles.stream()
105         .flatMap((Angle a) -> a.getBondList().stream())
106         .noneMatch(constrainedBonds::contains);
107     nConstraints = nBonds + (3 * nAngles);
108     atoms1 = new int[nConstraints];
109     atoms2 = new int[nConstraints];
110     lengths = new double[nConstraints];
111 
112     for (int i = 0; i < nBonds; i++) {
113       Bond bi = constrainedBonds.get(i);
114       atoms1[i] = bi.getAtom(0).getXyzIndex() - 1;
115       atoms2[i] = bi.getAtom(1).getXyzIndex() - 1;
116       lengths[i] = bi.bondType.distance;
117     }
118 
119     /*logger.info(" Atoms 1: " + Arrays.toString(atoms1));
120     logger.info(" Atoms 2: " + Arrays.toString(atoms2));
121     logger.info(" Lengths: " + Arrays.toString(lengths));*/
122 
123     for (int i = 0; i < nAngles; i++) {
124       int iAng = nBonds + (3 * i);
125       Angle ai = constrainedAngles.get(i);
126       Atom center = ai.getCentralAtom();
127       Bond b1 = ai.getBond(0);
128       Bond b2 = ai.getBond(1);
129       Atom at0 = b1.get1_2(center);
130       Atom at2 = b2.get1_2(center);
131 
132       double angVal = ai.angleType.angle[ai.nh];
133       double dist1 = b1.bondType.distance;
134       double dist2 = b2.bondType.distance;
135       double dist3 = SettleConstraint.lawOfCosines(dist1, dist2, angVal);
136 
137       int index0 = at0.getXyzIndex() - 1;
138       int index1 = center.getXyzIndex() - 1;
139       int index2 = at2.getXyzIndex() - 1;
140 
141       atoms1[iAng] = index0;
142       atoms2[iAng] = index1;
143       lengths[iAng] = dist1;
144 
145       ++iAng;
146       atoms1[iAng] = index1;
147       atoms2[iAng] = index2;
148       lengths[iAng] = dist2;
149 
150       ++iAng;
151       atoms1[iAng] = index0;
152       atoms2[iAng] = index2;
153       lengths[iAng] = dist3;
154     }
155 
156     uniqueIndices =
157         IntStream.concat(Arrays.stream(atoms1), Arrays.stream(atoms2))
158             .sorted()
159             .distinct()
160             .toArray();
161 
162     OpenMapRealMatrix kSparse = new OpenMapRealMatrix(nConstraints, nConstraints);
163 
164     int nAtoms = allAtoms.length;
165 
166     // Points from an Atom index to the Set of all Constraint indices it's involved in.
167     List<Set<Integer>> atomsToConstraints = new ArrayList<>(nAtoms);
168     for (int i = 0; i < nAtoms; i++) {
169       // Can assume 4 constraints max, since CHONPS is rarely bonded to 5+ atoms.
170       atomsToConstraints.add(new HashSet<>(4));
171     }
172 
173     for (int i = 0; i < nConstraints; i++) {
174       atomsToConstraints.get(atoms1[i]).add(i);
175       atomsToConstraints.get(atoms2[i]).add(i);
176     }
177 
178     logger.info(
179         String.format(" Initial CCMA setup: %10.6g sec", 1.0E-9 * (time + System.nanoTime())));
180     long subTime = -System.nanoTime();
181 
182     for (int i = 0; i < nConstraints; i++) {
183       int atomi0 = atoms1[i];
184       int atomi1 = atoms2[i];
185       // DO YOU HAVE ANY IDEA HOW LONG IT TOOK FOR ME TO REALIZE MASSES WERE XYZ-INDEXED?
186       double invMassI0 = 1.0 / masses[atomi0 * 3];
187       double invMassI1 = 1.0 / masses[atomi1 * 3];
188       double sumInv = invMassI0 + invMassI1;
189 
190       // Indices of all Constraints that involve either atomi0 or atomi1.
191       Set<Integer> coupledConstraints = new HashSet<>(atomsToConstraints.get(atomi0));
192       coupledConstraints.addAll(atomsToConstraints.get(atomi1));
193 
194       // Iterate over all coupled Constraints, sharing at least one common Atom with constraint i.
195       for (int j : coupledConstraints) {
196         // Diagonal element, coupling is obviously 1.0.
197         if (i == j) {
198           kSparse.setEntry(i, j, 1.0);
199           continue;
200         }
201 
202         double scale;
203         int atomj0 = atoms1[j];
204         int atomj1 = atoms2[j];
205         int atoma; // Atom unique to constraint i.
206         int atomb; // Atom shared between both constraints.
207         int atomc; // Atom unique to constraint j.
208 
209         if (atomi0 == atomj0) {
210           assert atomi1 != atomj1;
211           atoma = atomi1;
212           atomb = atomi0;
213           atomc = atomj1;
214           scale = invMassI0 / sumInv;
215         } else if (atomi0 == atomj1) {
216           assert atomi1 != atomj0;
217           atoma = atomi1;
218           atomb = atomi0;
219           atomc = atomj0;
220           scale = invMassI0 / sumInv;
221         } else if (atomi1 == atomj0) {
222           // Yes IntelliJ, it should always be true. That's why I'm asserting it.
223           assert atomi0 != atomj1;
224           atoma = atomi0;
225           atomb = atomi1;
226           atomc = atomj1;
227           scale = invMassI1 / sumInv;
228         } else if (atomi1 == atomj1) {
229           assert atomi0 != atomj0;
230           atoma = atomi1;
231           atomb = atomi0;
232           atomc = atomj1;
233           scale = invMassI1 / sumInv;
234         } else {
235           throw new IllegalArgumentException(
236               " Despite by necessity sharing an atom, these constraints don't share an atom.");
237         }
238 
239         // We now have a pair of constraints a-b b-c. Find the a-b-c angle.
240 
241         // Search for a constraint a-c that closes the triangle.
242         boolean foundAngle = false;
243         for (int constraintK : atomsToConstraints.get(atoma)) {
244           if (atoms1[constraintK] == atomc || atoms2[constraintK] == atomc) {
245             double dab = lengths[i];
246             double dbc = lengths[j];
247             double dac = lengths[constraintK];
248 
249             // Apply the Law of Cosines to find the angle defined by the a-b b-c a-c lengths.
250             double angle = (dab * dab) + (dbc * dbc) - (dac * dac);
251             angle /= (2 * dab * dbc);
252             // The angle is formally the cosine of its current value, but all we need is that
253             // cosine.
254             double coupling = scale * angle;
255             kSparse.setEntry(i, j, coupling);
256             foundAngle = true;
257             break;
258           }
259         }
260 
261         // Otherwise, look for a force field term. In this case, the approximate matrix K
262         // will not quite match the true Jacobian matrix J.
263         if (!foundAngle) {
264           Atom atA = allAtoms[atoma];
265           Atom atB = allAtoms[atomb];
266           Atom atC = allAtoms[atomc];
267           Angle angleB = atA.getAngle(atB, atC);
268           double angVal = angleB.angleType.angle[angleB.nh];
269           double coupling = scale * FastMath.cos(FastMath.toRadians(angVal));
270           kSparse.setEntry(i, j, coupling);
271           foundAngle = true;
272         }
273 
274         if (!foundAngle) {
275           logger.severe(
276               String.format(" Could not find the angle between coupled constraints %d, %d", i, j));
277         }
278       }
279     }
280 
281     subTime += System.nanoTime();
282     logger.info(
283         String.format(" Time to construct K as a sparse matrix: %10.6g sec", 1.0E-9 * subTime));
284 
285     // K is constructed. Invert it.
286     // TODO: Find a fast way to invert sparse matrices, since this is at least O(n^2).
287     subTime = -System.nanoTime();
288     QRDecomposition qrd = new QRDecomposition(kSparse);
289     RealMatrix kInvDense = qrd.getSolver().getInverse();
290     subTime += System.nanoTime();
291     logger.info(String.format(" Time to invert K: %10.6g sec", 1.0E-9 * subTime));
292     subTime = -System.nanoTime();
293 
294     kInvSparse = new OpenMapRealMatrix(nConstraints, nConstraints);
295     IntStream.range(0, nConstraints)
296         .
297         // parallel(). // I have no idea if OpenMapRealMatrix is thread-safe.
298             forEach(
299             (int i) -> {
300               double[] rowI = kInvDense.getRow(i);
301               for (int j = 0; j < nConstraints; j++) {
302                 double val = rowI[j];
303                 if (Math.abs(val) > elementCutoff) {
304                   kInvSparse.setEntry(i, j, val);
305                 }
306               }
307             });
308 
309     // TODO: Delete this block.
310     double[][] dataDense = kInvDense.getData();
311     double[][] dataSparse = kInvSparse.getData();
312     logger.fine(" Dense array:");
313     for (int i = 0; i < nConstraints; i++) {
314       logger.fine(Arrays.toString(dataDense[i]));
315     }
316     logger.fine(" Sparse array:");
317     for (int i = 0; i < nConstraints; i++) {
318       logger.fine(Arrays.toString(dataSparse[i]));
319     }
320 
321     // TODO: Actually do this.
322     reducedMasses = new double[nConstraints];
323     for (int i = 0; i < nConstraints; i++) {
324       int atI = atoms1[i];
325       atI *= 3; // The mass array is XYZ-indexed, not atom-indexed.
326       int atJ = atoms2[i];
327       atJ *= 3;
328       double invMassI = 1.0 / masses[atI];
329       double invMassJ = 1.0 / masses[atJ];
330       reducedMasses[i] = 0.5 / (invMassI + invMassJ);
331     }
332   }
333 
334   /**
335    * Constructs a set of bond length Constraints to be satisfied using the Constaint Constraint
336    * Matrix Approximation, a parallelizable stable numeric method.
337    *
338    * <p>The nonzeroCutoff field specifies how large an element of K-1 needs to be to be kept;
339    * smaller elements (indicating weak constraint-constraint couplings) are set to zero to make K-1
340    * sparse and thus keep CCMA tractable.
341    *
342    * <p>The constrainedAngles will constrain both component bonds and the triangle-closing distance.
343    * The constrainedBonds list should not include any bonds included in the constrained angles.
344    *
345    * @param constrainedBonds Bonds to be constrained, not included in constrainedAngles
346    * @param constrainedAngles Angles to be constrained indirectly (by constructing a rigid
347    *     triangle).
348    * @param allAtoms All Atoms of the system, including unconstrained Atoms.
349    * @param masses All masses of the system, including unconstrained atom masses.
350    * @param nonzeroCutoff CCMA parameter defining how sparse/dense K-1 should be.
351    * @return Returns a new CcmaConstraint instance.
352    */
353   public static CcmaConstraint ccmaFactory(
354       List<Bond> constrainedBonds,
355       List<Angle> constrainedAngles,
356       final Atom[] allAtoms,
357       final double[] masses,
358       double nonzeroCutoff) {
359     CcmaConstraint newC =
360         new CcmaConstraint(constrainedBonds, constrainedAngles, allAtoms, masses, nonzeroCutoff);
361     constrainedBonds.forEach((Bond b) -> b.setConstraint(newC));
362     constrainedAngles.forEach((Angle a) -> a.setConstraint(newC));
363     return newC;
364   }
365 
366   /** {@inheritDoc} */
367   @Override
368   public void applyConstraintToStep(double[] xPrior, double[] xNew, double[] masses, double tol) {
369     applyConstraints(xPrior, xNew, masses, false, tol);
370   }
371 
372   /** {@inheritDoc} */
373   @Override
374   public void applyConstraintToVelocities(double[] x, double[] v, double[] masses, double tol) {
375     applyConstraints(x, v, masses, true, tol);
376   }
377 
378   /** {@inheritDoc} */
379   @Override
380   public int[] constrainedAtomIndices() {
381     return Arrays.copyOf(uniqueIndices, uniqueIndices.length);
382   }
383 
384   /** {@inheritDoc} */
385   @Override
386   public boolean constraintSatisfied(double[] x, double tol) {
387     return false;
388   }
389 
390   /** {@inheritDoc} */
391   @Override
392   public boolean constraintSatisfied(double[] x, double[] v, double xTol, double vTol) {
393     return false;
394   }
395 
396   /** {@inheritDoc} */
397   @Override
398   public int getNumDegreesFrozen() {
399     return nConstraints;
400   }
401 
402   /**
403    * Much of the math for applying to coordinates/velocities is the same. As such, OpenMM just uses a
404    * single driver method with a flag to indicate velocities or positions.
405    *
406    * @param xPrior Either pre-step coordinates (positions) or current coordinates (velocities)
407    * @param output Output vector, either constrained coordinates or velocities
408    * @param masses Masses of all particles
409    * @param constrainV Whether to apply constraints to velocities, or to coordinates.
410    * @param tol Numerical tolerance.
411    */
412   private void applyConstraints(
413       double[] xPrior, double[] output, double[] masses, boolean constrainV, double tol) {
414     if (xPrior == output) {
415       throw new IllegalArgumentException(" xPrior and output must be different arrays!");
416     }
417     long time = -System.nanoTime();
418 
419     /*// Distance vector for all constraints. TODO: Flatten to 1D array.
420     double[][] r_ij = new double[nConstraints][3];
421     // Distance magnitude for all constraints.
422     double[] d_ij = new double[nConstraints];
423     // Squared distance magnitude for all constraints.
424     double[] d_ij2 = new double[nConstraints];
425 
426     for (int i = 0; i < nConstraints; i++) {
427         int atom1 = 3 * atoms1[i];
428         int atom2 = 3 * atoms2[i];
429         for (int j = 0; j < 3; j++) {
430             r_ij[i][j] = xPrior[atom1 + j] - xPrior[atom2 + j];
431         }
432         d_ij2[i] = VectorMath.dot(r_ij[i], r_ij[i]);
433     }
434 
435     double lowerTol = 1 - 2*tol + tol*tol;
436     // TODO: Determine if adding tol+tol to upperTol is a typo in OpenMM. Also why they do it in the first place.
437     double upperTol = 1 + 2*tol + tol*tol;
438 
439     int nConverged = 0;
440     double[] constraintDelta = new double[nConstraints];
441     double[] tempDelta = new double[nConstraints];
442 
443     // Main CCMA loop.
444     for (int constraintIter = 0; constraintIter <= maxIters; constraintIter++) {
445         if (constraintIter >= maxIters) {
446             throw new IllegalArgumentException(String.format(" CCMA constraint failed to converge in %d iterations!", maxIters));
447         }
448         nConverged = 0;
449         double rmsd = 0;
450         for (int i = 0; i < nConstraints; i++) {
451             int atom1 = atoms1[i] * 3;
452             int atom2 = atoms2[i] * 3;
453 
454             // Separation vector I-J at this iteration.
455             double[] rp_ij = new double[3];
456             for (int j = 0; j < 3; j++) {
457                 rp_ij[j] = output[atom1 + j] - output[atom2 + j];
458             }
459             if (constrainV) {
460                 double rrpr = VectorMath.dot(rp_ij, r_ij[i]);
461                 constraintDelta[i] = -2 * reducedMasses[i] * rrpr / d_ij2[i];
462                 if (Math.abs(constraintDelta[i]) <= tol) {
463                     ++nConverged;
464                 }
465             } else {
466                 double rp2 = VectorMath.dot(rp_ij, rp_ij);
467                 double dist2 = lengths[i] * lengths[i];
468                 double diff = dist2 - rp2;
469                 double rrpr = VectorMath.dot(rp_ij, r_ij[i]);
470                 constraintDelta[i] = reducedMasses[i] * diff / rrpr;
471                 if (rp2 >= lowerTol * dist2 && rp2 <= upperTol * dist2) {
472                     ++nConverged;
473                 }
474             }
475         }
476 
477         // Test if the last iteration satisfied all constraints.
478         if (nConverged == nConstraints) {
479             break;
480         }
481 
482         if (kInvSparse != null) {
483             for (int i = 0; i < nConstraints; i++) {
484                 RealVector row = kInvSparse.getRowVector(i);
485                 RealVectorPreservingVisitor neo = new MatrixWalker(constraintDelta);
486                 double sum = row.walkInOptimizedOrder(neo);
487                 tempDelta[i] = sum;
488             }
489             System.arraycopy(tempDelta, 0, constraintDelta, 0, nConstraints);
490         }
491         for (int i = 0; i < nConstraints; i++) {
492             int atom1 = atoms1[i];
493             int atom2 = atoms2[i];
494             int at13 = 3 * atom1;
495             int at23 = 3 * atom2;
496 
497             double[] dr = new double[3];
498             VectorMath.scalar(r_ij[i], constraintDelta[i], dr);
499             for (int j = 0; j < 3; j++) {
500                 output[at13 + j] = dr[j] / masses[at13];
501                 output[at23 + j] = dr[j] / masses[at23];
502             }
503         }
504         logger.info(String.format(" %d converged at iteration %d", nConverged, constraintIter));
505     }
506     time += System.nanoTime();
507     logger.info(String.format(" Application of CCMA constraint: %10.4g sec", (time * 1.0E-9)));*/
508   }
509 
510   private static class MatrixWalker implements RealVectorPreservingVisitor {
511 
512     private final double[] constraintDelta; // DO NOT MODIFY.
513     private double sum = 0.0;
514 
515     MatrixWalker(final double[] cDelta) {
516       constraintDelta = cDelta;
517     }
518 
519     /** {@inheritDoc} */
520     @Override
521     public double end() {
522       return sum;
523     }
524 
525     /** {@inheritDoc} */
526     @Override
527     public void start(int dimension, int start, int end) {
528     }
529 
530     /** {@inheritDoc} */
531     @Override
532     public void visit(int index, double value) {
533       sum += (value * constraintDelta[index]);
534     }
535   }
536 }