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.utils;
39  
40  import ffx.numerics.math.Double3;
41  import ffx.potential.bonded.Atom;
42  import org.apache.commons.math3.geometry.euclidean.threed.Rotation;
43  import org.apache.commons.math3.geometry.euclidean.threed.RotationConvention;
44  import org.apache.commons.math3.geometry.euclidean.threed.RotationOrder;
45  import org.apache.commons.math3.linear.Array2DRowRealMatrix;
46  import org.apache.commons.math3.linear.EigenDecomposition;
47  
48  import java.util.Arrays;
49  import java.util.logging.Logger;
50  
51  import static java.lang.String.format;
52  import static org.apache.commons.math3.util.FastMath.PI;
53  
54  /**
55   * Structure Metrics contains functionality to calculate characteristics of coordinate systems.
56   * <p>
57   * This includes: Gyrate computes the radius of gyration of a molecular system from its atomic
58   * coordinates.
59   * <p>
60   * Inertia computes the principal moments of inertia for the system, and optionally translates the
61   * center of mass to the origin and rotates the principal axes onto the global axes. Reference:
62   * Herbert Goldstein, "Classical Mechanics, 2nd Edition", Addison-Wesley, Reading, MA, 1980; see the
63   * Euler angle xyz convention in Appendix B
64   *
65   * @author Michael J. Schnieders
66   * @author Aaron J. Nessler
67   * @since 1.0
68   */
69  public class StructureMetrics {
70  
71    private static final Logger logger = Logger.getLogger(StructureMetrics.class.getName());
72  
73    /**
74     * Compute the radius of gyration for all atoms in the supplied array.
75     *
76     * @param atoms Atom array.
77     * @return The radius of gyration.
78     */
79    public static double radiusOfGyration(Atom[] atoms) {
80      Double3 radius = new Double3(radiusOfGyrationComponents(atoms));
81      return radius.length();
82    }
83  
84    /**
85     * Compute the average radius of gyration.
86     *
87     * @param x Array of atomic X-coordinates.
88     * @param y Array of atomic X-coordinates.
89     * @param z Array of atomic X-coordinates.
90     * @return The radius of gyration.
91     */
92    public static double radiusOfGyration(double[] x, double[] y, double[] z, double[] mass) {
93      assert (x.length == y.length);
94      assert (y.length == z.length);
95      Double3 radius = new Double3(radiusOfGyrationComponents(x, y, z, mass, true));
96      return radius.length();
97    }
98  
99    /**
100    * Compute the average radius of gyration.
101    *
102    * @param xyz Array of atomic coordinates (xyz = [X0, Y0, Z0, X1, Y1, Z1, ...].
103    * @return The radius of gyration.
104    */
105   public static double radiusOfGyration(double[] xyz, double[] mass) {
106     assert (xyz.length % 3 == 0);
107     Double3 radius = new Double3(radiusOfGyrationComponents(xyz, mass, true));
108     return radius.length();
109   }
110 
111   /**
112    * Compute the radius of gyration for all atoms in the supplied array.
113    *
114    * @param atoms Atom array.
115    * @return The radius of gyration.
116    */
117   public static double[] radiusOfGyrationComponents(Atom[] atoms) {
118     int nAtoms = atoms.length;
119     double[] x = new double[nAtoms];
120     double[] y = new double[nAtoms];
121     double[] z = new double[nAtoms];
122     double[] mass = new double[nAtoms];
123 
124     int index = 0;
125     for (int i = 0; i < nAtoms; i++) {
126       mass[i] = atoms[i].getMass();
127       x[index] = atoms[i].getX();
128       y[index] = atoms[i].getY();
129       z[index] = atoms[i].getZ();
130       index++;
131     }
132 
133     return radiusOfGyrationComponents(x, y, z, mass, false);
134   }
135 
136   /**
137    * Compute the components that make up the radius of gyration about yz-, xz-, xy-planes.
138    *
139    * @param xyz Coordinates for calculation
140    * @return radius of gyration along axes
141    */
142   public static double[] radiusOfGyrationComponents(double[] xyz, double[] mass, boolean pmp) {
143     assert (xyz.length % 3 == 0);
144     int nAtoms = xyz.length / 3;
145     // Find the centroid of the atomic coordinates.
146     double[] x = new double[nAtoms];
147     double[] y = new double[nAtoms];
148     double[] z = new double[nAtoms];
149 
150     for (int i = 0; i < nAtoms; i++) {
151       int index = i * 3;
152       x[i] = xyz[index++];
153       y[i] = xyz[index++];
154       z[i] = xyz[index];
155     }
156 
157     return radiusOfGyrationComponents(x, y, z, mass, pmp);
158   }
159 
160   /**
161    * Compute the components that make up the radius of gyration tensor about yz-, xz-, xy-planes.
162    *
163    * @param x   Coordinates for calculation
164    * @param y   Coordinates for calculation
165    * @param z   Coordinates for calculation
166    * @param pmp Principal moment plane
167    * @return radius of gyration about planes (yz, xz, xy).
168    */
169   public static double[] radiusOfGyrationComponents(double[] x, double[] y, double[] z,
170                                                     double[] mass, boolean pmp) {
171     assert (x.length == y.length);
172     assert (y.length == z.length);
173     assert (x.length <= mass.length);
174     double massSum = Arrays.stream(mass).sum();
175 
176     // Find the centroid of the atomic coordinates.
177     int nAtoms = x.length;
178     double xc = 0.0;
179     double yc = 0.0;
180     double zc = 0.0;
181     for (int i = 0; i < nAtoms; i++) {
182       xc += x[i] * mass[i];
183       yc += y[i] * mass[i];
184       zc += z[i] * mass[i];
185     }
186     Double3 centroid = new Double3(xc, yc, zc);
187     centroid.scaleI(1.0 / massSum);
188 
189     // Compute the radius of gyration.
190     Double3 radius = new Double3();
191     if (pmp) {
192       // gyration tensor about principal planes
193       double xterm;
194       double yterm;
195       double zterm;
196       double xx = 0.0;
197       double xy = 0.0;
198       double xz = 0.0;
199       double yy = 0.0;
200       double yz = 0.0;
201       double zz = 0.0;
202       for (int i = 0; i < nAtoms; i++) {
203         xterm = x[i] - xc;
204         yterm = y[i] - yc;
205         zterm = z[i] - zc;
206         xx += xterm * xterm;
207         xy += xterm * yterm;
208         xz += xterm * zterm;
209         yy += yterm * yterm;
210         yz += yterm * zterm;
211         zz += zterm * zterm;
212       }
213       double[][] tensor = new double[3][3];
214       tensor[0][0] = xx;
215       tensor[0][1] = xy;
216       tensor[0][2] = xz;
217       tensor[1][0] = xy;
218       tensor[1][1] = yy;
219       tensor[1][2] = yz;
220       tensor[2][0] = xz;
221       tensor[2][1] = yz;
222       tensor[2][2] = zz;
223       // Diagonalize the matrix.
224       Array2DRowRealMatrix cMatrix = new Array2DRowRealMatrix(tensor, false);
225       EigenDecomposition eigenDecomposition = new EigenDecomposition(cMatrix);
226       // Extract the quaternions.
227       radius.set(eigenDecomposition.getRealEigenvalues()).scaleI(1.0 / nAtoms).sqrtI();
228 //      vec = eigenDecomposition.getV().getData();
229 //
230 //      // Select the direction for each principal moment axis
231 //      double dot = 0.0;
232 //      for (int i = 0; i < 2; i++) {
233 //        for (int j = 0; j < nAtoms; j++) {
234 //          xterm = vec[i][0] * (x[j] - xc);
235 //          yterm = vec[i][1] * (y[j] - yc);
236 //          zterm = vec[i][2] * (z[j] - zc);
237 //          dot = xterm + yterm + zterm;
238 //          if (dot < 0.0) {
239 //            for (int k = 0; k < 3; k++) {
240 //              vec[i][k] = -vec[i][k];
241 //            }
242 //          }
243 //          if (dot != 0.0) {
244 //            break;
245 //          }
246 //        }
247 //        if (dot != 0.0) {
248 //          break;
249 //        }
250 //      }
251     } else {
252       for (int i = 0; i < nAtoms; i++) {
253         Double3 xyz = new Double3(x[i], y[i], z[i]);
254         xyz.subI(centroid);
255         radius.addI(xyz.squareI());
256       }
257       radius.scaleI(1.0 / nAtoms).sqrtI();
258     }
259 
260     return radius.get();
261   }
262 
263   /**
264    * Compute the moments of inertia for all atoms in the supplied array.
265    *
266    * @param atoms Atom array.
267    * @param moved Move coordinates
268    * @param print Display values to user
269    * @param pma   Use principal moment axes.
270    * @return The moments of inertia.
271    */
272   public static double[][] momentsOfInertia(Atom[] atoms, boolean moved, boolean print,
273                                             boolean pma) {
274     double[] mass = new double[atoms.length];
275     int nAtoms = atoms.length;
276     double[] x = new double[nAtoms];
277     double[] y = new double[nAtoms];
278     double[] z = new double[nAtoms];
279 
280     int index = 0;
281     for (Atom atom : atoms) {
282       mass[index] = atom.getMass();
283       x[index] = atom.getX();
284       y[index] = atom.getY();
285       z[index] = atom.getZ();
286       index++;
287     }
288 
289     return momentsOfInertia(x, y, z, mass, moved, print, pma);
290   }
291 
292   /**
293    * Compute the moments of inertia.
294    *
295    * @param xyz   Array of atomic coordinates (xyz = [X0, Y0, Z0, X1, Y1, Z1, ...].
296    * @param mass  Mass of atoms
297    * @param moved Move from original coordinates to selection
298    * @param print Display values to user
299    * @param pma   Use principal moment axes.
300    * @return The radius of gyration.
301    */
302   public static double[][] momentsOfInertia(double[] xyz, double[] mass, boolean moved,
303                                             boolean print, boolean pma) {
304     assert (xyz.length % 3 == 0);
305     int nAtoms = xyz.length / 3;
306     // Find the centroid of the atomic coordinates.
307     double[] x = new double[nAtoms];
308     double[] y = new double[nAtoms];
309     double[] z = new double[nAtoms];
310 
311     for (int i = 0; i < nAtoms; i++) {
312       int index = i * 3;
313       x[i] = xyz[index++];
314       y[i] = xyz[index++];
315       z[i] = xyz[index];
316     }
317 
318     return momentsOfInertia(x, y, z, mass, moved, print, pma);
319   }
320 
321   /**
322    * Compute the moments of inertia
323    *
324    * @param x     Array of atomic X-coordinates.
325    * @param y     Array of atomic X-coordinates.
326    * @param z     Array of atomic X-coordinates.
327    * @param mass  mass of atoms
328    * @param moved Move coordinates to principal axes
329    * @param print Print out values to screen
330    * @param pma   Report moments of inertia to principal axes.
331    * @return The moment of inertia.
332    */
333   public static double[][] momentsOfInertia(double[] x, double[] y, double[] z, double[] mass,
334                                             boolean moved, boolean print, boolean pma) {
335     assert (x.length == y.length);
336     assert (y.length == z.length);
337 
338     // Find the centroid of the atomic coordinates.
339     double total = 0.0;
340     double xcm = 0.0;
341     double ycm = 0.0;
342     double zcm = 0.0;
343     int nAtoms = x.length;
344     for (int i = 0; i < nAtoms; i++) {
345       double massValue = mass[i];
346       total += massValue;
347       xcm += x[i] * massValue;
348       ycm += y[i] * massValue;
349       zcm += z[i] * massValue;
350     }
351     xcm /= total;
352     ycm /= total;
353     zcm /= total;
354 
355     // Compute and then diagonalize the inertia tensor
356     double xx = 0.0;
357     double xy = 0.0;
358     double xz = 0.0;
359     double yy = 0.0;
360     double yz = 0.0;
361     double zz = 0.0;
362 
363     double xterm;
364     double yterm;
365     double zterm;
366 
367     for (int i = 0; i < nAtoms; i++) {
368       double massValue = mass[i];
369       xterm = x[i] - xcm;
370       yterm = y[i] - ycm;
371       zterm = z[i] - zcm;
372       xx += xterm * xterm * massValue;
373       xy += xterm * yterm * massValue;
374       xz += xterm * zterm * massValue;
375       yy += yterm * yterm * massValue;
376       yz += yterm * zterm * massValue;
377       zz += zterm * zterm * massValue;
378     }
379     double[][] tensor = new double[3][3];
380     tensor[0][0] = yy + zz;
381     tensor[0][1] = -xy;
382     tensor[0][2] = -xz;
383     tensor[1][0] = -xy;
384     tensor[1][1] = xx + zz;
385     tensor[1][2] = -yz;
386     tensor[2][0] = -xz;
387     tensor[2][1] = -yz;
388     tensor[2][2] = xx + yy;
389 
390     double[] moment;
391     double[][] vec;
392     if (pma) {
393       // Diagonalize the matrix.
394       Array2DRowRealMatrix cMatrix = new Array2DRowRealMatrix(tensor, false);
395       EigenDecomposition eigenDecomposition = new EigenDecomposition(cMatrix);
396       // Extract the quaternions.
397       moment = eigenDecomposition.getRealEigenvalues();
398       vec = eigenDecomposition.getV().getData();
399 
400       // Select the direction for each principal moment axis
401       double dot = 0.0;
402       for (int i = 0; i < 2; i++) {
403         for (int j = 0; j < nAtoms; j++) {
404           xterm = vec[i][0] * (x[j] - xcm);
405           yterm = vec[i][1] * (y[j] - ycm);
406           zterm = vec[i][2] * (z[j] - zcm);
407           dot = xterm + yterm + zterm;
408           if (dot < 0.0) {
409             for (int k = 0; k < 3; k++) {
410               vec[i][k] = -vec[i][k];
411             }
412           }
413           if (dot != 0.0) {
414             break;
415           }
416         }
417         if (dot != 0.0) {
418           break;
419         }
420       }
421 
422       // Moment axes must give a right-handed coordinate system.
423       xterm = vec[0][0] * (vec[1][1] * vec[2][2] - vec[2][1] * vec[1][2]);
424       yterm = vec[0][1] * (vec[2][0] * vec[1][2] - vec[1][0] * vec[2][2]);
425       zterm = vec[0][2] * (vec[1][0] * vec[2][1] - vec[2][0] * vec[1][1]);
426       dot = xterm + yterm + zterm;
427       if (dot < 0.0) {
428         for (int i = 0; i < 3; i++) {
429           vec[2][i] = -vec[2][i];
430         }
431       }
432 
433       // Principal moment axes form rows of Euler rotation matrix.
434       if (moved) {
435         double[][] a = new double[3][3];
436         for (int i = 0; i < 3; i++) {
437           for (int j = 0; j < 3; j++) {
438             a[j][i] = vec[j][i];
439           }
440         }
441         // Translate to origin, then apply Euler rotation matrix.
442         for (int i = 0; i < nAtoms; i++) {
443           xterm = x[i] - xcm;
444           yterm = y[i] - ycm;
445           zterm = z[i] - zcm;
446           x[i] = a[0][0] * xterm + a[1][0] * yterm + a[2][0] * zterm;
447           y[i] = a[0][1] * xterm + a[1][1] * yterm + a[2][1] * zterm;
448           z[i] = a[0][2] * xterm + a[1][2] * yterm + a[2][2] * zterm;
449         }
450       }
451     } else {
452       vec = new double[][]{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}};
453       moment = new double[]{tensor[0][0], tensor[1][1], tensor[2][2]};
454     }
455 
456     // print the center of mass and Euler angle values
457     if (print) {
458       logger.info(format("\n Center of Mass Coordinates: %8.4f %8.4f %8.4f", xcm, ycm, zcm));
459       // invert vec
460       double[] angles = new Rotation(vec, 1.0E-7).getAngles(
461           RotationOrder.XYZ, RotationConvention.VECTOR_OPERATOR);
462       double radian = 180 / PI;
463       // Convert to degrees
464       for (int i = 0; i < 3; i++) {
465         angles[i] *= radian;
466       }
467       logger.info(format(" Euler Angles (Phi/Theta/Psi): %8.3f %8.3f %8.3f", angles[0], angles[1], angles[2]));
468       logger.info(
469           " Moments of Inertia and Principle Axes:\n  Moments (amu Ang^2): \t X-, Y-, and Z-Components of Axes:");
470       for (int i = 0; i < 3; i++) {
471         logger.info(format("  %16.3f %12.6f %12.6f %12.6f", moment[i], vec[i][0], vec[i][1], vec[i][2]));
472       }
473     }
474 
475     double[][] momentsAndVectors = new double[3][4];
476     for (int i = 0; i < 3; i++) {
477       for (int j = 0; j < 4; j++) {
478         if (j == 0) {
479           momentsAndVectors[i][j] = moment[i];
480         } else {
481           momentsAndVectors[i][j] = vec[i][j - 1];
482         }
483       }
484     }
485 
486     return momentsAndVectors;
487   }
488 }