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-2026.
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.algorithms.commands.test;
39  
40  import ffx.algorithms.cli.AlgorithmsCommand;
41  import ffx.numerics.Potential;
42  import ffx.potential.bonded.Atom;
43  import ffx.potential.bonded.Molecule;
44  import ffx.utilities.FFXBinding;
45  import picocli.CommandLine.Command;
46  import picocli.CommandLine.Option;
47  import picocli.CommandLine.Parameters;
48  
49  import java.util.ArrayList;
50  import java.util.Arrays;
51  import java.util.List;
52  
53  import java.util.*;
54  import static java.lang.Math.*;
55  import org.apache.commons.math3.linear.Array2DRowRealMatrix;
56  import org.apache.commons.math3.linear.EigenDecomposition;
57  import static java.lang.String.format;
58  
59  /**
60   * The FindRestraints script identifies guest molecule atoms that should be restrained based on
61   * their proximity to specific atoms in a host molecule.
62   * <br>
63   * Usage:
64   * <br>
65   * ffxc test.FindRestraints [options] &lt;filename&gt;
66   */
67  @Command(description = "Find guest atoms for COM or Boresch restraints.",
68          name = "test.FindRestraints")
69  public class FindRestraints extends AlgorithmsCommand {
70  
71    /**
72     * --hostName Molecule name of the host in the file.
73     */
74    @Option(names = {"--hostName"}, paramLabel = "BCD", defaultValue = "BCD",
75        description = "Host molecule name in the file.")
76    private String hostName;
77  
78    /**
79     * --guestName Molecule name of the guest in the file.
80     */
81    @Option(names = {"--guestName"}, paramLabel = "LIG", defaultValue = "LIG",
82        description = "Ligand molecule name in the file.")
83    private String guestName;
84  
85    /**
86     * --distanceCutoff Cutoff to use when selecting guest atoms near host COM.
87     */
88    @Option(names = {"--distanceCutoff"}, paramLabel = "5", defaultValue = "5",
89        description = "Cutoff to use when selecting guest atoms near host COM")
90    private double distanceCutoff;
91  
92    // ------------------------
93    // Boresch Options
94    // ------------------------
95  
96    @Option(names = {"--boresch"}, defaultValue = "false",
97        description = "Use Boresch anchor selection instead of COM-based selection.")
98    private boolean boresch;
99  
100   // From GHOAT paper, H1, H2, H3 are explicitly defined by the user
101   @Option(names = {"--H1"}, paramLabel = "int",
102       description = "Host anchor H1 atom index.")
103   private Integer H1Index;
104 
105   @Option(names = {"--H2"}, paramLabel = "int",
106       description = "Host anchor H2 atom index.")
107   private Integer H2Index;
108 
109   @Option(names = {"--H3"}, paramLabel = "int",
110       description = "Host anchor H3 atom index.")
111   private Integer H3Index;
112 
113 
114   @Option(names = {"--l1Range"}, defaultValue = "3.5",
115       description = "Cylinder diameter/height for G1 search.")
116   private double l1Range;
117 
118   @Option(names = {"--minAdis"}, defaultValue = "1.5",
119       description = "Minimum distance between guest anchors.")
120   private double minAdis;
121 
122   @Option(names = {"--maxAdis"}, defaultValue = "5.0",
123       description = "Maximum distance between guest anchors.")
124   private double maxAdis;
125 
126   /**
127    * One or more filenames.
128    */
129   @Parameters(arity = "1..*", paramLabel = "files",
130       description = "XYZ or PDB input files.")
131   private List<String> filenames;
132 
133   /**
134    * Creation of a public field to try and make the JUnit test work, original code does not declare this as a public field.
135    * Originally it is declared in the run method
136    */
137   public Potential potential;
138 
139   /**
140    * Get the potential object.
141    * @return The potential object.
142    */
143   public Potential getPotentialObject() {
144     return potential;
145   }
146 
147   /**
148    * FindRestraints Constructor.
149    */
150   public FindRestraints() {
151     super();
152   }
153 
154   /**
155    * FindRestraints Constructor.
156    * @param binding The Binding to use.
157    */
158   public FindRestraints(FFXBinding binding) {
159     super(binding);
160   }
161 
162   /**
163    * FindRestraints constructor that sets the command line arguments.
164    * @param args Command line arguments.
165    */
166   public FindRestraints(String[] args) {
167     super(args);
168   }
169 
170   /**
171    * {@inheritDoc}
172    */
173   @Override
174   public FindRestraints run() {
175     if (!init()) {
176       return this;
177     }
178 
179     activeAssembly = getActiveAssembly(filenames.get(0));
180     if (activeAssembly == null) {
181       logger.info(helpString());
182       return this;
183     }
184     Molecule host = null;
185     Molecule guest = null;
186 
187     for (Molecule mol : activeAssembly.getMoleculeArray()) {
188       if (mol.getName().contains(hostName)) host = mol;
189       if (mol.getName().contains(guestName)) guest = mol;
190     }
191 
192     if (host == null || guest == null) {
193       logger.severe("Host or Guest molecule not found.");
194       return this;
195     }
196 
197     if (boresch) {
198       runBoreschMode(host, guest);
199     } else {
200       runCOMMode();
201     }
202 
203     return this;
204   }
205 
206   // ================================================================
207   // ==================== COM MODE =================================
208   // ================================================================
209   private void runCOMMode() {   
210     Molecule[] molArr = activeAssembly.getMoleculeArray();
211 
212     List<Atom> restrainHostList = new ArrayList<>();
213     List<Atom> restrainList = new ArrayList<>();
214     double[] COM = new double[3];
215     double[] subCOM = new double[3];
216 
217     int[] restrainHostIndices = new int[]{11, 16, 17, 20, 23, 26, 31, 32, 39, 40, 51, 63, 64, 70, 71, 82, 94, 95, 101, 102, 113, 125, 126, 132, 133, 144, 156, 157, 163, 164, 175, 187, 188, 191, 198};
218     for (Molecule molecule : molArr) {
219       logger.info(format(" Molecule name: " + molecule.getName()));
220       if (molecule.getName().contains(hostName)) {
221         Atom[] host_atoms = molecule.getAtomList().toArray(new Atom[0]);
222         COM = getCOM(host_atoms);
223         logger.info(format(" Center of mass of host molecule: " + Arrays.toString(COM)));
224         for (Atom atom : host_atoms) {
225           if (contains(restrainHostIndices, atom.getIndex())) {
226             restrainHostList.add(atom);
227             logger.info(format(" Atom: " + atom));
228           }
229         }
230         Atom[] subAtoms = restrainHostList.toArray(new Atom[0]);
231         subCOM = getCOM(subAtoms);
232         logger.info(format(" Center of mass of subsection host atoms: " + Arrays.toString(subCOM)));
233         double comdist = Math.sqrt(Math.pow(subCOM[0] - COM[0], 2) +
234             Math.pow(subCOM[1] - COM[1], 2) +
235             Math.pow(subCOM[2] - COM[2], 2));
236         logger.info(format(" Distance between COMs: " + comdist));
237       } else if (molecule.getName().contains(guestName)) {
238         Atom[] guest_atoms = molecule.getAtomList().toArray(new Atom[0]);
239         for (Atom atom : guest_atoms) {
240           double dist = Math.sqrt(Math.pow(atom.getXYZ().get()[0] - subCOM[0], 2) +
241               Math.pow(atom.getXYZ().get()[1] - subCOM[1], 2) +
242               Math.pow(atom.getXYZ().get()[2] - subCOM[2], 2));
243           //logger.info(format("Atom: " + atom));
244           //logger.info(format("XYZ: " + atom.getXYZ().get()));
245           //logger.info(format("Distance from host COM: " + dist));
246 
247           if (dist < distanceCutoff && atom.isHeavy()) {
248             restrainList.add(atom);
249           }
250         }
251       }
252     }
253     logger.info(format(" Number of atoms to restrain: " + restrainList.size()));
254     int[] restrainIndices = restrainList.stream()
255         .mapToInt(Atom::getIndex)
256         .toArray();
257     logger.info(format(" Restrain list indices: " + Arrays.toString(restrainIndices)));
258 
259   }
260 
261   // ================================================================
262   // ==================== BORESCH MODE ==============================
263   // ================================================================
264 
265     /**
266      * Calculate rotation matrix to align host to Z-axis.
267      *
268      * The returned matrix has rows = {x̂, ŷ, ẑ} so that r' = R * r.
269      */
270     private double[][] getAlignmentRotationMatrix(Atom[] hostAtoms, double[] com) {
271       // Compute inertia tensor
272       double[][] I = new double[3][3];
273 
274       for (Atom atom : hostAtoms) {
275         double m = atom.getMass();
276         double[] r = atom.getXYZ().get();
277         // Use coordinates relative to COM
278         double rx = r[0] - com[0];
279         double ry = r[1] - com[1];
280         double rz = r[2] - com[2];
281 
282         I[0][0] += m * (ry * ry + rz * rz);
283         I[1][1] += m * (rx * rx + rz * rz);
284         I[2][2] += m * (rx * rx + ry * ry);
285 
286         I[0][1] -= m * rx * ry;
287         I[0][2] -= m * rx * rz;
288         I[1][2] -= m * ry * rz;
289       }
290 
291       I[1][0] = I[0][1];
292       I[2][0] = I[0][2];
293       I[2][1] = I[1][2];
294 
295       // Diagonalize inertia tensor
296       EigenDecomposition ed = new EigenDecomposition(new Array2DRowRealMatrix(I));
297       double[] evals = ed.getRealEigenvalues();
298       double[][] evecs = ed.getV().getData(); // columns are eigenvectors
299 
300       int zIndex = 0;
301       int xIndex = 0;
302       for (int i = 1; i < 3; i++) {
303         if (evals[i] > evals[zIndex]) {
304           zIndex = i;
305         }
306         if (evals[i] < evals[xIndex]) {
307           xIndex = i;
308         }
309       }
310       int yIndex = 3 - zIndex - xIndex;
311 
312       double[] zHat = normalize(new double[]{evecs[0][zIndex], evecs[1][zIndex], evecs[2][zIndex]});
313       double[] xHat = normalize(new double[]{evecs[0][xIndex], evecs[1][xIndex], evecs[2][xIndex]});
314       double[] yHat = normalize(cross(zHat, xHat));
315       xHat = normalize(cross(yHat, zHat));
316 
317       return new double[][]{xHat, yHat, zHat};
318     }
319 
320   /**
321    * Apply translation and rotation transformation to atoms
322    */
323     private void applyTransformation(Atom[] atoms, double[] com, double[][] rotationMatrix) {
324       for (Atom atom : atoms) {
325         double[] r = atom.getXYZ().get();
326 
327         // Translate to origin
328         double tx = r[0] - com[0];
329         double ty = r[1] - com[1];
330         double tz = r[2] - com[2];
331 
332         // Rotate: r' = R * r
333         double[] rotated = new double[3];
334         for (int i = 0; i < 3; i++) {
335           rotated[i] = rotationMatrix[i][0] * tx
336               + rotationMatrix[i][1] * ty
337               + rotationMatrix[i][2] * tz;
338         }
339 
340         atom.setXYZ(rotated);
341       }
342     }
343 
344     private static double[][] captureCoordinates(Atom[] atoms) {
345       double[][] coords = new double[atoms.length][3];
346       for (int i = 0; i < atoms.length; i++) {
347         double[] r = atoms[i].getXYZ().get();
348         coords[i][0] = r[0];
349         coords[i][1] = r[1];
350         coords[i][2] = r[2];
351       }
352       return coords;
353     }
354 
355     private static void restoreCoordinates(Atom[] atoms, double[][] coords) {
356       for (int i = 0; i < atoms.length; i++) {
357         atoms[i].setXYZ(coords[i]);
358       }
359     }
360 
361   private void runBoreschMode(Molecule host, Molecule guest) {
362 
363     if (H1Index == null || H2Index == null || H3Index == null) {
364       logger.severe("Boresch mode requires --H1 --H2 --H3 indices.");
365       return;
366     }
367 
368     Atom H1 = findAtomByIndex(host, H1Index);
369     Atom H2 = findAtomByIndex(host, H2Index);
370     Atom H3 = findAtomByIndex(host, H3Index);
371 
372     if (H1 == null || H2 == null || H3 == null) {
373       logger.severe("Could not locate host anchor atoms.");
374       return;
375     }
376 
377     // Calculate principal axis from HOST non-hydrogen atoms ONLY
378     Atom[] hostAtoms = host.getAtomList().toArray(new Atom[0]);
379     Atom[] guestAtoms = guest.getAtomList().toArray(new Atom[0]);
380     
381     // Use COM of non-hydrogen host atoms as the origin
382     double[] com = getCOMNonHydrogen(hostAtoms);
383     logger.info(format("Host COM (non-H): (%.3f, %.3f, %.3f)", com[0], com[1], com[2]));
384     
385     double[][] rotationMatrix = getAlignmentRotationMatrix(hostAtoms, com);
386 
387     // Temporarily align to select anchors in a consistent frame
388     double[][] hostCoords = captureCoordinates(hostAtoms);
389     double[][] guestCoords = captureCoordinates(guestAtoms);
390     applyTransformation(hostAtoms, com, rotationMatrix);
391     applyTransformation(guestAtoms, com, rotationMatrix);
392 
393     Atom G1 = selectG1(guest);
394     if (G1 == null) return;
395     
396     Atom G2 = selectG2(guest, G1);
397     if (G2 == null) return;
398     
399     Atom G3 = selectG3(guest, G1, G2);
400     if (G3 == null) return;
401 
402     // Restore original coordinates for geometry evaluation
403     restoreCoordinates(hostAtoms, hostCoords);
404     restoreCoordinates(guestAtoms, guestCoords);
405 
406     // Log selected atoms
407     logger.info(format("\n=== Selected Boresch Anchors ==="));
408     logger.info(format("Host anchors:"));
409     logger.info(format("  H1: %s (index %d, residue %d)", H1.getName(), H1.getIndex(), H1.getResidueNumber()));
410     logger.info(format("  H2: %s (index %d, residue %d)", H2.getName(), H2.getIndex(), H2.getResidueNumber()));
411     logger.info(format("  H3: %s (index %d, residue %d)", H3.getName(), H3.getIndex(), H3.getResidueNumber()));
412     logger.info(format("Guest anchors:"));
413     logger.info(format("  G1: %s (index %d, residue %d)", G1.getName(), G1.getIndex(), G1.getResidueNumber()));
414     logger.info(format("  G2: %s (index %d, residue %d)", G2.getName(), G2.getIndex(), G2.getResidueNumber()));
415     logger.info(format("  G3: %s (index %d, residue %d)", G3.getName(), G3.getIndex(), G3.getResidueNumber()));
416 
417     // Note: G1, G2, G3 are geometric anchor atoms and do not require bonding constraints
418 
419     // Calculate all geometric parameters
420     double r = distance(H1.getXYZ().get(), G1.getXYZ().get());
421     double thetaA = angle(H2, H1, G1);
422     double thetaB = angle(H1, G1, G2);
423     double phiA = dihedral(H3, H2, H1, G1);
424     double phiB = dihedral(H2, H1, G1, G2);
425     double phiC = dihedral(H1, G1, G2, G3);
426 
427     logger.info(format("\n=== Boresch Restraint Parameters ==="));
428     logger.info(format("Distance H1-G1 (r): %.3f Å", r));
429     logger.info(format("Angle H2-H1-G1 (θA): %.2f°", thetaA));
430     logger.info(format("Angle H1-G1-G2 (θB): %.2f°", thetaB));
431     logger.info(format("Dihedral H3-H2-H1-G1 (φA): %.2f°", phiA));
432     logger.info(format("Dihedral H2-H1-G1-G2 (φB): %.2f°", phiB));
433     logger.info(format("Dihedral H1-G1-G2-G3 (φC): %.2f°", phiC));
434     
435     // Validate geometry (warn about near-degenerate angles)
436     if (thetaA < 20.0 || thetaA > 160.0) {
437         logger.warning(format("Angle θA = %.2f° is near-degenerate.", thetaA));
438     }
439     if (thetaB < 20.0 || thetaB > 160.0) {
440         logger.warning(format("Angle θB = %.2f° is near-degenerate.", thetaB));
441     }
442   }
443 
444 
445 
446   /**
447    * Calculate dihedral angle between four atoms
448    */
449   private static double dihedral(Atom a, Atom b, Atom c, Atom d) {
450       double[] v1 = subtract(a.getXYZ().get(), b.getXYZ().get());
451       double[] v2 = subtract(c.getXYZ().get(), b.getXYZ().get());
452       double[] v3 = subtract(d.getXYZ().get(), c.getXYZ().get());
453       
454       double[] n1 = cross(v1, v2);
455       double[] n2 = cross(v2, v3);
456       
457       double[] m1 = cross(n1, v2);
458       
459       double x = dot(n1, n2);
460       double y = dot(m1, n2);
461       
462       return toDegrees(atan2(y, x));
463   }
464 
465   private static double[] cross(double[] a, double[] b) {
466       return new double[]{
467           a[1]*b[2] - a[2]*b[1],
468           a[2]*b[0] - a[0]*b[2],
469           a[0]*b[1] - a[1]*b[0]
470       };
471   }
472 
473   private static double dot(double[] a, double[] b) {
474       return a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
475   }
476 
477 
478   // ================================================================
479   // ==================== GUEST SELECTION ===========================
480   // ================================================================
481 
482   private Atom selectG1(Molecule guest) {
483       double radius = l1Range / 2.0;
484       double radius2 = radius * radius;
485       double halfHeight = l1Range / 2.0;
486 
487       // MISSING: You need to declare these variables
488       Atom G1 = null;
489       double bestR2 = Double.MAX_VALUE;
490 
491       // MISSING: You need to iterate through guest atoms
492       for (Atom atom : guest.getAtomList()) {
493           if (!atom.isHeavy()) continue;
494 
495           double[] xyz = atom.getXYZ().get();
496           double r2 = xyz[0]*xyz[0] + xyz[1]*xyz[1];
497 
498           if (Math.abs(xyz[2]) <= halfHeight && r2 <= radius2) {
499               if (r2 < bestR2) {
500                   bestR2 = r2;
501                   G1 = atom;
502               }
503           }
504       }
505       
506       if (G1 == null) {
507           logger.severe("anch1 error: No valid G1 anchor found.");
508           return null;  
509       }
510 
511       return G1;  
512   }
513 
514   private Atom selectG2(Molecule guest, Atom G1) {
515 
516     Atom best = null;
517     double bestProj = Double.MAX_VALUE;
518 
519     double[] g1 = G1.getXYZ().get();
520 
521     for (Atom atom : guest.getAtomList()) {
522       if (atom == G1 || !atom.isHeavy()) continue;
523 
524       double d = distance(g1, atom.getXYZ().get());
525       if (d < minAdis || d > maxAdis) continue;
526 
527       double[] diff = subtract(atom.getXYZ().get(), g1);
528       double proj2 = diff[0] * diff[0] + diff[1] * diff[1];
529 
530       if (proj2 < bestProj) {
531         best = atom;
532         bestProj = proj2;
533       }
534     }
535     if (best == null) {
536         logger.severe("anch2 error: Could not identify valid G2 anchor.");
537     }
538 
539     return best;
540   }
541 
542   private Atom selectG3(Molecule guest, Atom G1, Atom G2) {
543       Atom best = null;
544       double bestAngleDiff = Double.MAX_VALUE;
545 
546       List<Atom> candidates = new ArrayList<>();
547 
548       for (Atom atom : guest.getAtomList()) {
549           if (atom == G1 || atom == G2 || !atom.isHeavy()) continue;
550 
551           // Check distance from G2
552           double d = distance(G2.getXYZ().get(), atom.getXYZ().get());
553           if (d < minAdis || d > maxAdis) continue;
554 
555           // Check distance from G1
556           double d_g1 = distance(G1.getXYZ().get(), atom.getXYZ().get());
557           if (d_g1 < minAdis) continue;
558 
559           candidates.add(atom);
560       }
561       
562       if (candidates.isEmpty()) {
563           logger.warning("No valid G3 candidates found");
564           return null;
565       }
566       
567       // Find atom with angle closest to 90°
568       for (Atom atom : candidates) {
569           double ang = angle(G1, G2, atom);
570           
571           // Reject degenerate angles
572           if (ang < 20.0 || ang > 160.0) continue;
573           
574           double diff = Math.abs(ang - 90.0);
575           
576           if (diff < bestAngleDiff) {
577               bestAngleDiff = diff;
578               best = atom;
579           }
580       }
581       
582       if (best == null) {
583           logger.warning("No G3 candidate with acceptable angle found");
584       } else {
585           logger.info(String.format("Selected G3: %s (index %d)", 
586               best.getName(), best.getIndex()));
587       }
588       
589       return best;
590   }
591 
592   // ================================================================
593   // ==================== GEOMETRY HELPERS ==========================
594   // ================================================================
595 
596   private static double distance(double[] a, double[] b) {
597     return sqrt(pow(a[0] - b[0], 2)
598             + pow(a[1] - b[1], 2)
599             + pow(a[2] - b[2], 2));
600   }
601 
602   private static double[] subtract(double[] a, double[] b) {
603     return new double[]{a[0] - b[0], a[1] - b[1], a[2] - b[2]};
604   }
605 
606   private static double[] normalize(double[] v) {
607     double norm = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
608     if (norm == 0.0) {
609       return new double[]{0.0, 0.0, 0.0};
610     }
611     return new double[]{v[0] / norm, v[1] / norm, v[2] / norm};
612   }
613 
614   private static double angle(Atom a, Atom b, Atom c) {
615     double[] v1 = subtract(a.getXYZ().get(), b.getXYZ().get());
616     double[] v2 = subtract(c.getXYZ().get(), b.getXYZ().get());
617 
618     double dot = v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2];
619     double mag1 = sqrt(v1[0]*v1[0] + v1[1]*v1[1] + v1[2]*v1[2]);
620     double mag2 = sqrt(v2[0]*v2[0] + v2[1]*v2[1] + v2[2]*v2[2]);
621 
622     return toDegrees(acos(dot / (mag1 * mag2)));
623   }
624 
625   private static Atom findAtomByIndex(Molecule mol, int index) {
626     for (Atom atom : mol.getAtomList()) {
627       if (atom.getIndex() == index) return atom;
628     }
629     return null;
630   }
631 
632   /**
633    * Gets the center of mass of a set of atoms.
634    *
635    * @param atoms Array of atoms
636    * @return x,y,z coordinates of center of mass
637    */
638   private static double[] getCOM(Atom[] atoms) {
639     // Get center of mass of moleculeOneAtoms
640     double[] COM = new double[3];
641     double totalMass = 0.0;
642     for (Atom s : atoms) {
643       double[] pos = s.getXYZ().get();
644       COM[0] += pos[0] * s.getMass();
645       COM[1] += pos[1] * s.getMass();
646       COM[2] += pos[2] * s.getMass();
647       totalMass += s.getMass();
648     }
649     totalMass = 1 / totalMass;
650     COM[0] *= totalMass;
651     COM[1] *= totalMass;
652     COM[2] *= totalMass;
653 
654     return COM;
655   }
656 
657   /**
658    * Gets the center of mass of non-hydrogen atoms in a set.
659    *
660    * @param atoms Array of atoms
661    * @return x,y,z coordinates of center of mass (non-hydrogen only)
662    */
663   private static double[] getCOMNonHydrogen(Atom[] atoms) {
664     double[] COM = new double[3];
665     double totalMass = 0.0;
666     for (Atom s : atoms) {
667       if (!s.isHeavy()) continue;  // Skip hydrogen atoms
668       double[] pos = s.getXYZ().get();
669       COM[0] += pos[0] * s.getMass();
670       COM[1] += pos[1] * s.getMass();
671       COM[2] += pos[2] * s.getMass();
672       totalMass += s.getMass();
673     }
674     if (totalMass == 0.0) {
675       return new double[]{0.0, 0.0, 0.0};
676     }
677     totalMass = 1 / totalMass;
678     COM[0] *= totalMass;
679     COM[1] *= totalMass;
680     COM[2] *= totalMass;
681 
682     return COM;
683   }
684 
685   /**
686    * Checks if an int array contains a specific value.
687    *
688    * @param array Array to search
689    * @param value Value to find
690    * @return true if array contains value
691    */
692   private static boolean contains(int[] array, int value) {
693     return Arrays.stream(array).anyMatch(element -> element == value);
694   }
695 }