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.utils;
39  
40  import static ffx.crystal.SymOp.Tr_0_0_0;
41  import static ffx.crystal.SymOp.ZERO_ROTATION;
42  import static ffx.crystal.SymOp.applyCartesianSymOp;
43  import static ffx.crystal.SymOp.asMatrixString;
44  import static ffx.crystal.SymOp.invertSymOp;
45  import static ffx.numerics.math.ScalarMath.mod;
46  import static ffx.numerics.math.DoubleMath.dist2;
47  import static ffx.numerics.math.MatrixMath.mat4Mat4;
48  import static ffx.potential.parsers.DistanceMatrixFilter.writeDistanceMatrixRow;
49  import static ffx.potential.utils.StructureMetrics.momentsOfInertia;
50  import static ffx.potential.utils.StructureMetrics.radiusOfGyration;
51  import static ffx.potential.utils.StructureMetrics.radiusOfGyrationComponents;
52  import static ffx.potential.utils.Superpose.applyRotation;
53  import static ffx.potential.utils.Superpose.applyTranslation;
54  import static ffx.potential.utils.Superpose.calculateRotation;
55  import static ffx.potential.utils.Superpose.calculateTranslation;
56  import static ffx.potential.utils.Superpose.rmsd;
57  import static ffx.potential.utils.Superpose.rotate;
58  import static ffx.potential.utils.Superpose.translate;
59  import static ffx.utilities.StringUtils.parseAtomRanges;
60  import static ffx.utilities.Resources.logResources;
61  import static ffx.utilities.StringUtils.writeAtomRanges;
62  import static java.lang.String.format;
63  import static java.lang.System.arraycopy;
64  import static java.util.Arrays.copyOf;
65  import static java.util.Arrays.fill;
66  import static java.util.Arrays.sort;
67  import static org.apache.commons.io.FilenameUtils.getName;
68  import static org.apache.commons.math3.util.FastMath.abs;
69  import static org.apache.commons.math3.util.FastMath.sqrt;
70  import static org.apache.commons.math3.util.FastMath.max;
71  import static org.apache.commons.math3.util.FastMath.ceil;
72  import static org.apache.commons.math3.util.FastMath.min;
73  import static org.apache.commons.math3.util.FastMath.cbrt;
74  import static org.apache.commons.math3.util.FastMath.PI;
75  
76  import edu.rit.mp.DoubleBuf;
77  import edu.rit.pj.Comm;
78  import ffx.crystal.Crystal;
79  import ffx.crystal.ReplicatesCrystal;
80  import ffx.crystal.SymOp;
81  import ffx.numerics.math.RunningStatistics;
82  import ffx.potential.MolecularAssembly;
83  import ffx.potential.Utilities;
84  import ffx.potential.bonded.Atom;
85  import ffx.potential.bonded.Bond;
86  import ffx.potential.bonded.MSNode;
87  import ffx.potential.parameters.ForceField;
88  import ffx.potential.parsers.DistanceMatrixFilter;
89  import ffx.potential.parsers.PDBFilter;
90  import ffx.potential.parsers.SystemFilter;
91  import ffx.potential.parsers.XYZFilter;
92  import ffx.utilities.DoubleIndexPair;
93  import ffx.utilities.IndexIndexPair;
94  
95  import java.io.BufferedWriter;
96  import java.io.File;
97  import java.io.FileWriter;
98  import java.util.ArrayList;
99  import java.util.Arrays;
100 import java.util.List;
101 import java.util.logging.Level;
102 import java.util.logging.Logger;
103 import java.util.stream.IntStream;
104 
105 import org.apache.commons.configuration2.CompositeConfiguration;
106 import org.apache.commons.io.FileUtils;
107 import org.apache.commons.io.FilenameUtils;
108 import org.apache.commons.lang3.ArrayUtils;
109 
110 /**
111  * Class ProgressiveAlignmentOfCrystals holds the majority of the functionality necessary to quantify
112  * crystal similarity following the PAC method.
113  *
114  * @author Okimasa OKADA, Aaron J. Nessler,  and Michael J. Schnieders
115  * @since 1.0
116  */
117 public class ProgressiveAlignmentOfCrystals {
118 
119   /**
120    * Logger for the ProgressiveAlignmentOfCrystals Class.
121    */
122   private static final Logger logger = Logger.getLogger(
123       ProgressiveAlignmentOfCrystals.class.getName());
124   /**
125    * SystemFilter containing structures for first file.
126    */
127   private final SystemFilter baseFilter;
128   /**
129    * Number of structures stored in SystemFilter of first file.
130    */
131   private final int baseSize;
132   /**
133    * Label for the first file.
134    */
135   private final String baseLabel;
136   /**
137    * SystemFilter containing structures for second file.
138    */
139   private final SystemFilter targetFilter;
140   /**
141    * Number of structures stored in SystemFilter for second file.
142    */
143   private final int targetSize;
144   /**
145    * Label for the second file.
146    */
147   private final String targetLabel;
148   /**
149    * The amount of work per row for each process.
150    */
151   private final int numWorkItems;
152   /**
153    * If this flag is true, then the RMSD matrix is symmetric (e.g., comparing an archive of
154    * structures to itself).
155    */
156   private final boolean isSymmetric;
157   /**
158    * Label to use for the RMSD logging
159    */
160   private String rmsdLabel;
161   /**
162    * Row of RMSD values (length = targetSize).
163    */
164   public final double[] distRow;
165   /**
166    * The default restart row is 0. A larger value may be set by the "readMatrix" method if a restart
167    * is requested.
168    */
169   private int restartRow = 0;
170   /**
171    * The default restart column is 0. Larger values being set by the "readMatrix" method during
172    * restarts are not currently supported. A restart must begin from the beginning of a row for
173    * simplicity.
174    */
175   private int restartColumn = 0;
176   /**
177    * Parallel Java world communicator.
178    */
179   private final Comm world;
180   /**
181    * If false, do not use MPI communication.
182    */
183   private final boolean useMPI;
184   /**
185    * Number of processes.
186    */
187   private final int numProc;
188   /**
189    * Rank of this process.
190    */
191   private final int rank;
192   /**
193    * The distances matrix stores a single RMSD value from each process. The array is of size
194    * [numProc][1].
195    */
196   private final double[][] distances;
197   /**
198    * Each distance is wrapped inside a DoubleBuf for MPI communication.
199    */
200   private final DoubleBuf[] buffers;
201   /**
202    * Convenience reference to the RMSD value for this process.
203    */
204   private final double[] myDistances;
205   /**
206    * Convenience reference for the DoubleBuf of this process.
207    */
208   private final DoubleBuf myBuffer;
209   /**
210    * Masses for N asymmetric units (nAU * nCoords).
211    */
212   private double[] massN;
213   /**
214    * Sum of masses for one asymmetric unit.
215    */
216   private double massSum = 0;
217   /**
218    * Indices of atoms from first crystal to be used in comparison (Z' = 1).
219    */
220   private int[] comparisonAtoms;
221   /**
222    * List of each AU index's distance to central AU for first crystal (Maps baseXYZ).
223    */
224   private DoubleIndexPair[] baseAUDist;
225   /**
226    * Working copy of baseAUDist, updated when treating a new AU as center.
227    */
228   private DoubleIndexPair[] baseAUDist_2;
229   /**
230    * List of each AU index's distance to central AU for second crystal (Maps targetXYZ).
231    */
232   private DoubleIndexPair[] targetAUDist;
233   /**
234    * Working copy of targetAUDist, updated when treating a new AU as center.
235    */
236   private DoubleIndexPair[] targetAUDist_2;
237   /**
238    * List containing indices for each unique AU in current crystal.
239    */
240   private final List<Integer> tempListIndices = new ArrayList<>();
241   /**
242    * Array containing indices for each unique AU in first crystal (Maps baseAUDist).
243    */
244   private Integer[] uniqueBase;
245   /**
246    * Array containing indices for each unique AU in second crystal (Maps targetAUDist).
247    */
248   private Integer[] uniqueTarget;
249   /**
250    * Array containing XYZ coordinates for first crystal sorted based on distance to replicates
251    * center.
252    */
253   private double[] baseXYZoriginal;
254   /**
255    * Array containing XYZ coordinates for first crystal sorted based on distance to replicates
256    * center. Working copy of baseXYZoriginal that undergoes rotations/translations during
257    * comparisons.
258    */
259   private double[] baseXYZ;
260   /**
261    * Array containing XYZ coordinates for second crystal sorted based on distance to replicates
262    * center.
263    */
264   private double[] targetXYZoriginal;
265   /**
266    * Array containing XYZ coordinates for second crystal sorted based on distance to replicates
267    * center. Working copy of targetXYZoriginal that undergoes rotations/translations during
268    * comparisons.
269    */
270   private double[] targetXYZ;
271   /**
272    * Center of masses (or geometric center) for every AU in first replicates crystal (comparison, AU
273    * num, XYZ).
274    */
275   private double[][][] baseCoM;
276   /**
277    * Original coordinates for central AU of first crystal (first index Z', second index X, Y, Z
278    * coords). These coordinates should match coordinates in input file.
279    */
280   private double[][] baseAUoriginal;
281   /**
282    * Coordinates for central AU of first crystal (X, Y, Z coords).
283    */
284   private double[] baseAU;
285   /**
286    * Coordinates for central 3 AUs of first crystal  ( X, Y, Z coords).
287    */
288   private double[] base3AUs;
289   /**
290    * Coordinates for central N AUs of first crystal  (comparison value, second index X, Y, Z
291    * coords).
292    */
293   private double[][] baseNAUs;
294   /**
295    * Coordinates of N AUs from first crystal of the closest match (lowest RMSD).
296    */
297   private double[][][] bestBaseNAUs;
298   /**
299    * Center of masses (or geometric center) for every AU in second replicates crystal
300    */
301   private double[][] targetCoM;
302   /**
303    * Original coordinates for central AU of second crystal (first index Z', second index X, Y, Z
304    * coords). These coordinates should match coordinates in input file.
305    */
306   private double[][] targetAUoriginal;
307   /**
308    * Coordinates for central AU of second crystal (first index Z', second index X, Y, Z coords).
309    */
310   private double[] targetAU;
311   /**
312    * Coordinates for central 3 AUs of second crystal (first index Z', second index X, Y, Z coords).
313    */
314   private double[] target3AUs;
315   /**
316    * Coordinates for central N AUs of second crystal (first index Z', second index X, Y, Z coords).
317    */
318   private double[] targetNAUs;
319   /**
320    * Coordinates of N AUs from second crystal of the closest match (lowest RMSD) (first index Z',
321    * second index X, Y, Z coords).
322    */
323   private double[][][] bestTargetNAUs;
324   /**
325    * Indices and distances of matching AUs between first and second crystal.
326    */
327   private DoubleIndexPair[] pairedAUs;
328   /**
329    * Number of comparisons that are below a given tolerance.
330    */
331   private int numberOfHits = 0;
332   /**
333    * Default PAC caches files to promote efficiency (otherwise use low memory flag).
334    */
335   private File[] fileCache;
336   /**
337    * Default PAC caches file names to promote efficiency (otherwise use low memory flag).
338    */
339   private String[] nameCache;
340   /**
341    * Default PAC caches bonds to promote efficiency (otherwise use low memory flag).
342    */
343   private ArrayList<ArrayList<Bond>> bondCache;
344   /**
345    * Default PAC caches atoms to promote efficiency (otherwise use low memory flag).
346    */
347   private Atom[][] atomCache;
348   /**
349    * Default PAC caches force field properties to promote efficiency (otherwise use low memory
350    * flag).
351    */
352   private ForceField[] forceFieldCache;
353   /**
354    * Default PAC caches crystals to promote efficiency (otherwise use low memory flag).
355    */
356   private Crystal[] crystalCache;
357   /**
358    * Radius of gyration for the best matching clusters (position 0 for first and 1 for second
359    * crystal).
360    */
361   private final double[] gyrations = new double[2];
362   /**
363    * Moments of inertia and vectors of application for first crystal.
364    */
365   private double[][] bestBaseMandV = new double[3][4];
366   /**
367    * Moments of inertia and vectors of application for second crystal.
368    */
369   private double[][] bestTargetMandV = new double[3][4];
370   /**
371    * Radius of gyration components for first crystal.
372    */
373   private double[] bestBaseRg = new double[3];
374   /**
375    * Radius of gyration components for second crystal.
376    */
377   private double[] bestTargetRg = new double[3];
378   /**
379    * Working copy of replicates Sym Op applied to asymmetric unit of first system.
380    */
381   private SymOp baseSymOp;
382   /**
383    * Replicates Sym Op applied to asymmetric unit of first system for best comparison.
384    */
385   private SymOp[][] bestBaseSymOp;
386   /**
387    * Working copy of replicates Sym Op applied to asymmetric unit of second system.
388    */
389   private SymOp targetSymOp;
390   /**
391    * Replicates Sym Op applied to asymmetric unit of second system for best comparison.
392    */
393   private SymOp[][] bestTargetSymOp;
394   /**
395    * List of symmetry operators used to create replicates crystal for 1st structure.
396    */
397   private ArrayList<SymOp> baseSymOps = new ArrayList<>();
398   /**
399    * Index order closest to replicates (get(0) closest to rep center; indexOf(i) ith Sym Op).
400    */
401   private ArrayList<Integer> baseDistMap = new ArrayList<>();
402   /**
403    * Index order closest to replicates (get(0) closest to rep center; indexOf(i) ith Sym Op).
404    */
405   private ArrayList<SymOp> targetSymOps = new ArrayList<>();
406   /**
407    * List of indices created through expansion of replicates crystal for 2nd structures.
408    */
409   private ArrayList<Integer> targetDistMap = new ArrayList<>();
410   /**
411    * Boolean to determine if comparison should print symmetry operators used.
412    */
413   private double printSym;
414   /**
415    * String builder to attempt to off load work from logger.
416    */
417   private final StringBuilder stringBuilder = new StringBuilder();
418   /**
419    * Working copy of symmetry operator used to generate central base molecule updated each
420    * iteration.
421    */
422   private SymOp baseTransformSymOp;
423   /**
424    * Working copy of symmetry operator used to generate central target molecules updated each
425    * iteration.
426    */
427   private SymOp targetTransformSymOp;
428   /**
429    * Best symmetry operator used to generate central base molecule.
430    */
431   private SymOp[][] bestBaseTransformSymOp;
432   /**
433    * Best symmetry operator used to generate central target molecules.
434    */
435   private SymOp[][] bestTargetTransformSymOp;
436   /**
437    * If molecules between two crystals differ below this tolerance, they are treated as equivalent.
438    */
439   private static final double MATCH_TOLERANCE = 1.0E-12;
440 
441   /**
442    * Adjusted tolerance used when printing symmetry operators.
443    */
444   private static final double SYM_TOLERANCE = 2.0E-8;
445 
446   /**
447    * Constructor for the ProgressiveAlignmentOfCrystals class.
448    *
449    * @param baseFilter   SystemFilter containing a set of crystal structures to compare.
450    * @param targetFilter SystemFilter containing the other set of crystals to compare.
451    * @param isSymmetric  Whether the comparison can be limited to the upper triangle.
452    */
453   public ProgressiveAlignmentOfCrystals(SystemFilter baseFilter, SystemFilter targetFilter,
454                                         boolean isSymmetric) {
455     // Setup filter for both crystal files to compare.
456     this.baseFilter = baseFilter;
457     this.targetFilter = targetFilter;
458     // Whether comparison is symmetric (i.e., do not double compare forward/backward).
459     this.isSymmetric = isSymmetric;
460 
461     // Number of models to be evaluated.
462     baseSize = baseFilter.countNumModels();
463     baseLabel = getName(baseFilter.getFile().getAbsolutePath());
464     targetSize = targetFilter.countNumModels();
465     targetLabel = getName(targetFilter.getFile().getAbsolutePath());
466 
467     assert !isSymmetric || (baseSize == targetSize);
468 
469     if (logger.isLoggable(Level.FINER)) {
470       logger.finer(format("\n Conformations for %s: %d", baseLabel, baseSize));
471       logger.finer(format(" Conformations for %s: %d", targetLabel, targetSize));
472     }
473 
474     // Distance matrix to store compared values (dimensions are "human readable" [m x n]).
475     distRow = new double[targetSize];
476 
477     CompositeConfiguration properties = baseFilter.getActiveMolecularSystem().getProperties();
478     useMPI = properties.getBoolean("pj.use.mpi", true);
479 
480     if (useMPI) {
481       world = Comm.world();
482       // Number of processes is equal to world size (often called size).
483       numProc = world.size();
484       // Each processor gets its own rank (ID of sorts).
485       rank = world.rank();
486     } else {
487       world = null;
488       numProc = 1;
489       rank = 0;
490     }
491 
492     // Padding of the target array size (inner loop limit) is for parallelization.
493     // Target conformations are parallelized over available nodes.
494     // For example, if numProc = 8 and targetSize = 12, then paddedTargetSize = 16.
495     int extra = targetSize % numProc;
496     int paddedTargetSize = targetSize;
497     if (extra != 0) {
498       paddedTargetSize = targetSize - extra + numProc;
499     }
500     numWorkItems = paddedTargetSize / numProc;
501 
502     if (numProc > 1) {
503       logger.info(format(" Number of MPI Processes:  %d", numProc));
504       logger.info(format(" Rank of this MPI Process: %d", rank));
505       logger.info(format(" Work per process per row: %d", numWorkItems));
506     }
507 
508     // Initialize array to -1.0 as -1.0 is not a viable RMSD.
509     fill(distRow, -1.0);
510 
511     // Each process will complete the following amount of work per row.
512     distances = new double[numProc][numWorkItems];
513 
514     // Initialize each distance as -2.0.
515     for (int i = 0; i < numProc; i++) {
516       fill(distances[i], -2.0);
517     }
518 
519     // DoubleBuf is a wrapper used by MPI Comm methods to transfer data between processors.
520     buffers = new DoubleBuf[numProc];
521     for (int i = 0; i < numProc; i++) {
522       buffers[i] = DoubleBuf.buffer(distances[i]);
523     }
524 
525     // Convenience reference to the storage for each process.
526     myDistances = distances[rank];
527     myBuffer = buffers[rank];
528   }
529 
530   /**
531    * Perform single comparison between two crystals (staticAssembly and mobileAssembly).
532    *
533    * @param file1              First file object containing crystals to compare
534    * @param name1              Crystal structure (1st or base) that will remain relatively static (only
535    *                           translations).
536    * @param bondList1          List of bonds in crystal used to save XYZ files.
537    * @param atoms1             Atoms in first crystal used to save XYZ files.
538    * @param forceField1        Force field values used to save files/print symmetry operators
539    * @param file2              Second file object containing crystals to compare
540    * @param name2              Crystal structure (2nd or target) that will rotate to match static assembly.
541    * @param bondList2          List of bonds in crystal used to save XYZ files.
542    * @param atoms2             Atoms in first crystal used to save XYZ files.
543    * @param forceField2        Force field values used to save files/print symmetry operators
544    * @param z1                 Number of molecules in asymmetric unit of first crystal.
545    * @param z2                 Number of molecules in asymmetric unit of second crystal.
546    * @param compareAtomsSize   Number of active atoms in asymmetric unit of first crystal.
547    * @param nAU                Number of asymmetric units to compare.
548    * @param baseSearchValue    Number of anticipated unique entities in 1st system.
549    * @param targetSearchValue  Number of anticipated unique entities in 2nd system.
550    * @param matchTol           Tolerance to determine whether two AUs are the same.
551    * @param compNum            Comparison number based on all file submitted (logging).
552    * @param strict             Compare all unique AUs between crystals.
553    * @param saveClusters               Save out files of compared crystals.
554    * @param machineLearning    Save out PDBs and CSVs of compared crystals.
555    * @param linkage            Criteria to select nearest AUs (0=single, 1=average, 2=complete linkage).
556    * @param inertia            Compute and display components of inertia tensor.
557    * @param gyrationComponents Compute and display gyration components (asphericity,
558    *                           acylindricity, anisotropy, etc.)
559    * @return the computed RMSD.
560    */
561   private double compare(final File file1, final String name1, final List<Bond> bondList1,
562                          final Atom[] atoms1, final ForceField forceField1, final File file2, final String name2,
563                          final List<Bond> bondList2, final Atom[] atoms2, final ForceField forceField2, final int z1,
564                          final int z2, final int compareAtomsSize, final int nAU, final int baseSearchValue,
565                          final int targetSearchValue, final double matchTol, final int compNum, final boolean strict,
566                          final int saveClusters, final boolean machineLearning, final int linkage, final boolean inertia,
567                          final boolean gyrationComponents) {
568     // TODO: Does PAC work for a combination of molecules and polymers?
569     // It does not compare them on an individual basis, but can compare AUs as a whole (set zp/zp2 to 1).
570     boolean useSave = saveClusters > 0;
571     boolean useSym = printSym >= 0.0;
572     int nCoords = compareAtomsSize * 3;
573     // Number of species in expanded crystals.
574     int nBaseMols = baseXYZ.length / nCoords;
575     int nTargetMols = targetXYZ.length / nCoords;
576 
577     if (logger.isLoggable(Level.FINER)) {
578       stringBuilder.append(format("\n Base: %s Target: %s", name1, name2));
579       stringBuilder.append(format("""
580 
581            Comparing %3d of %5d in base sphere.
582            Comparing %3d of %5d in target sphere.
583           """, nAU, nBaseMols, nAU, nTargetMols));
584     }
585 
586     if (useSym) {
587       // Allocate variables used for Sym Op
588       if (bestBaseSymOp == null || bestBaseSymOp.length != z1 || bestBaseSymOp[0].length != z2) {
589         bestBaseSymOp = new SymOp[z1][z2];
590       }
591       if (bestTargetSymOp == null || bestTargetSymOp.length != z1
592           || bestTargetSymOp[0].length != z2) {
593         bestTargetSymOp = new SymOp[z1][z2];
594       }
595       if (bestBaseTransformSymOp == null || bestBaseTransformSymOp.length != z1
596           || bestBaseTransformSymOp[0].length != z2) {
597         bestBaseTransformSymOp = new SymOp[z1][z2];
598       }
599       if (bestTargetTransformSymOp == null || bestTargetTransformSymOp.length != z1
600           || bestTargetTransformSymOp[0].length != z2) {
601         bestTargetTransformSymOp = new SymOp[z1][z2];
602       }
603     }
604 
605     // Reorder molDist2 as we shift a different molecule (m) to the center each loop.
606     if (logger.isLoggable(Level.FINER)) {
607       stringBuilder.append(" Prioritize target system.\n");
608       //Determine if AUs in first system are same hand as center most in first (stereoisomer handling).
609       stringBuilder.append(" Search conformations of each crystal:\n");
610     }
611     // NOTE not Z' representatives, just first Z closest AUs.
612     arraycopy(baseXYZ, baseAUDist[0].index() * nCoords, baseAU, 0, nCoords);
613 
614     tempListIndices.clear();
615     // Compare replicates to initial molecule to determine unique conformations.
616     numberUniqueAUs(baseXYZ, baseAUDist, baseAU.clone(), nCoords, baseSearchValue, strict, nBaseMols,
617         massN, matchTol);
618     int baseLength = tempListIndices.size();
619     if (uniqueBase == null || uniqueBase.length != baseLength) {
620       uniqueBase = new Integer[baseLength];
621     }
622     tempListIndices.toArray(uniqueBase);
623 
624     // NOTE not Z' representatives, just first Z closest AUs.
625     arraycopy(targetXYZ, targetAUDist[0].index() * nCoords, targetAU, 0, nCoords);
626     tempListIndices.clear();
627     // Compare replicates to initial molecule to match unique conformations.
628     numberUniqueAUs(targetXYZ, targetAUDist, targetAU.clone(), nCoords, targetSearchValue, strict,
629         nTargetMols, massN, matchTol);
630     int targetLength = tempListIndices.size();
631     if (uniqueTarget == null || uniqueTarget.length != targetLength) {
632       uniqueTarget = new Integer[targetLength];
633     }
634     tempListIndices.toArray(uniqueTarget);
635     final boolean reverse = baseLength > targetLength;
636     if (logger.isLoggable(Level.FINE)) {
637       stringBuilder.append(format("""
638 
639             %d conformations detected out of %d in base crystal.
640             %d conformations detected out of %d in target crystal.
641           """, baseLength, baseSearchValue, targetLength, targetSearchValue));
642     }
643 
644     if (useSym) {
645       // Obtain coordinates contained in file (assumes MoveIntoUnitCell was performed before PAC).
646       if (z1 > 1 || z2 > 1) {
647         stringBuilder.append(
648             "\n Utilizing Sym Ops with Z''>1. Attempting to map all unique structures.\n");
649       }
650     }
651 
652     // Determine which unique AUs are most similar between crystals
653     // Minimum difference between each unique target AU and closest matching base AU.
654     double min1 = Double.MAX_VALUE;
655     double max1 = Double.MIN_VALUE;
656     int minBIndex = -1;
657     int minTIndex = -1;
658     int maxBIndex = -1;
659     int maxTIndex = -1;
660 
661     // Maintains index of minimum Z' differences for Sym Op prep
662     IndexIndexPair[][] minZinds = new IndexIndexPair[z1][z2];
663     for (IndexIndexPair[] row : minZinds) {
664       fill(row, new IndexIndexPair(-1, -1));
665     }
666 
667     // Maintains the minimum difference between Z' AUs
668     double[][] minZdiffs = new double[z1][z2];
669     for (double[] row : minZdiffs) {
670       fill(row, Double.MAX_VALUE);
671     }
672     // Array to store all RMSD_1 comparisons
673     double[][] minDiffs = new double[baseLength][targetLength];
674     for (double[] row : minDiffs) {
675       fill(row, Double.MAX_VALUE);
676     }
677     // For each of the unique base molecules
678     for (int i = 0; i < baseLength; i++) {
679       // OG index of the base.
680       int bIndex = uniqueBase[i];
681       // Indices based on distance from center of replicates.
682       int baseInd = baseAUDist[bIndex].index();
683       // Determine which asymmetric unit we are dealing with.
684       int currZ1 = baseDistMap.get(baseInd) % z1;
685 
686       double[] baseXYZ = new double[nCoords];
687       // For each of the unique target molecules
688       for (int j = 0; j < targetLength; j++) {
689         // Need to remove translation/rotation from previous check.
690         arraycopy(this.baseXYZ, baseInd * nCoords, baseXYZ, 0, nCoords);
691         // OG index of the target.
692         int tIndex = uniqueTarget[j];
693         // Indices based on distance from center of replicates.
694         int targetInd = targetAUDist[tIndex].index();
695         // Determine which asymmetric unit we are dealing with.
696         int currZ2 = targetDistMap.get(targetInd) % z2;
697 
698         double[] targetXYZ = new double[nCoords];
699         // Need to remove translation/rotation from previous check.
700         arraycopy(this.targetXYZ, targetInd * nCoords, targetXYZ, 0, nCoords);
701         // Determine translation to move base molecule to origin.
702         double[] tempTranB = calculateTranslation(baseXYZ, massN);
703         applyTranslation(baseXYZ, tempTranB);
704         // Determine translation to move target molecule to origin.
705         double[] tempTranT = calculateTranslation(targetXYZ, massN);
706         applyTranslation(targetXYZ, tempTranT);
707         // Determine rotation of target to match base.
708         double[][] tempRotT = calculateRotation(baseXYZ, targetXYZ, massN);
709         applyRotation(targetXYZ, tempRotT);
710         // Calculate RMSD_1
711         double value = rmsd(baseXYZ, targetXYZ, massN);
712 
713         if (logger.isLoggable(Level.FINEST)) {
714           stringBuilder.append(format(
715               "\n Comp %3d: Base %2d IND %3d (Z''=%2d) \t Target %2d IND %3d (Z''=%2d) Diff: %8.4f\n",
716               j + i * targetLength, bIndex, baseInd, currZ1, tIndex, targetInd, currZ2, value));
717           if (useSym) {
718             stringBuilder.append(" Base Temp Translation: ").append(Arrays.toString(tempTranB))
719                 .append("\n");
720             stringBuilder.append(" Target Temp Translation: ").append(Arrays.toString(tempTranT));
721             stringBuilder.append(matrixToString(tempRotT, j + i * targetLength, "Temp Rot"))
722                 .append("\n");
723           }
724         }
725 
726         minDiffs[i][j] = value;
727         // If only one molecule is requested, save values for logging/saving.
728         if (nAU == 1 && !strict) {
729           // Record values for short circuit
730           arraycopy(baseXYZ, 0, baseNAUs[0], 0, nAU * nCoords);
731           arraycopy(targetXYZ, 0, targetNAUs, 0, nAU * nCoords);
732           arraycopy(baseXYZ, 0, bestBaseNAUs[currZ1][currZ2], 0, nAU * nCoords);
733           arraycopy(targetXYZ, 0, bestTargetNAUs[currZ1][currZ2], 0, nAU * nCoords);
734         }
735         if (useSym && value < minZdiffs[currZ1][currZ2]) {
736           minZdiffs[currZ1][currZ2] = value;
737           minZinds[currZ1][currZ2] = new IndexIndexPair(baseInd, targetInd);
738           arraycopy(baseXYZ, 0, baseAU, 0, nCoords);
739           arraycopy(targetXYZ, 0, targetAU, 0, nCoords);
740           if (logger.isLoggable(Level.FINER)) {
741             stringBuilder.append(
742                 format("\n Base AU: %2d of %2d" + "\t\tvs\t\t Target AU: %2d of %2d \t (%9.4f)",
743                     currZ1 + 1, z1, currZ2 + 1, z2, minZdiffs[currZ1][currZ2]));
744             if (logger.isLoggable(Level.FINER)) {
745               stringBuilder.append("\n Base Translation: ").append(Arrays.toString(tempTranB))
746                   .append("\n");
747               stringBuilder.append(" Target Translation: ").append(Arrays.toString(tempTranT));
748               stringBuilder.append(matrixToString(tempRotT, currZ1, "Target Rotation"));
749             }
750           }
751           baseTransformSymOp = new SymOp(ZERO_ROTATION, tempTranB);
752           bestBaseTransformSymOp[currZ1][currZ2] = baseTransformSymOp;
753           // SymOp.append defaults to rotation then translation. Break apart to have translation then rotation.
754           targetTransformSymOp = new SymOp(ZERO_ROTATION, tempTranT);
755           targetTransformSymOp = targetTransformSymOp.append(new SymOp(tempRotT, Tr_0_0_0));
756           bestTargetTransformSymOp[currZ1][currZ2] = targetTransformSymOp;
757           SymOp base = baseSymOps.get(baseDistMap.get(minZinds[currZ1][currZ2].sortedIndex()));
758           baseSymOp = new SymOp(base.rot, base.tr);
759           SymOp target = targetSymOps.get(
760               targetDistMap.get(minZinds[currZ1][currZ2].referenceIndex()));
761           targetSymOp = new SymOp(target.rot, target.tr);
762           bestBaseSymOp[currZ1][currZ2] = baseSymOp;
763           bestTargetSymOp[currZ1][currZ2] = targetSymOp;
764 
765           if (logger.isLoggable(Level.FINEST)) {
766             stringBuilder.append("\n Base Sym Op Translation: ")
767                 .append(Arrays.toString(bestBaseSymOp[currZ1][currZ2].tr));
768             stringBuilder.append(
769                 matrixToString(bestBaseSymOp[currZ1][currZ2].rot, currZ2 + currZ1 * z2,
770                     "Base Sym Op Rot")).append("\n");
771             stringBuilder.append(" Base Transform Translation: ")
772                 .append(Arrays.toString(bestBaseTransformSymOp[currZ1][currZ2].tr));
773             stringBuilder.append(
774                 matrixToString(bestBaseTransformSymOp[currZ1][currZ2].rot, currZ2 + currZ1 * z2,
775                     "Base Transform Rot")).append("\n");
776             stringBuilder.append(" Target Sym Op Translation: ")
777                 .append(Arrays.toString(bestTargetSymOp[currZ1][currZ2].tr));
778             stringBuilder.append(
779                 matrixToString(bestTargetSymOp[currZ1][currZ2].rot, currZ2 + currZ1 * z2,
780                     "Target Sym Op Rot")).append("\n");
781             stringBuilder.append(" Target Transform Translation: ")
782                 .append(Arrays.toString(bestTargetTransformSymOp[currZ1][currZ2].tr));
783             stringBuilder.append(
784                 matrixToString(bestTargetTransformSymOp[currZ1][currZ2].rot, currZ2 + currZ1 * z2,
785                     "Target Transform Rot")).append("\n");
786           }
787           if (logger.isLoggable(Level.FINER)) {
788             stringBuilder.append(format(
789                 " \n Saved %3d: Base %2d IND %3d (Z''=%2d) \t Target %2d IND %3d (Z''=%2d) Diff: %8.4f\n",
790                 j + i * targetLength, bIndex, baseInd, currZ1, tIndex, targetInd, currZ2, value));
791           }
792         }
793         // Determine minimum value of all comparisons.
794         if (minDiffs[i][j] < min1) {
795           min1 = minDiffs[i][j];
796           minBIndex = bIndex;
797           minTIndex = tIndex;
798         }
799         // Determine maximum value of all comparisons.
800         if (minDiffs[i][j] > max1) {
801           max1 = minDiffs[i][j];
802           maxBIndex = bIndex;
803           maxTIndex = tIndex;
804         }
805       }
806     }
807     // Index of the most similar uniques.
808     int baseBestZ = baseDistMap.get(baseAUDist[minBIndex].index()) % z1;
809     int targetBestZ = targetDistMap.get(targetAUDist[minTIndex].index()) % z2;
810     if (logger.isLoggable(Level.FINER)) {
811       stringBuilder.append(
812           format("\n Base min index: %d (Z''=%d) Target min Index %d (Z''=%d)", minBIndex, baseBestZ,
813               minTIndex, targetBestZ));
814     }
815     // Identify which AUs are most similar between crystals.
816     int maxLength = max(baseLength, targetLength);
817     int[] baseTargetMap = new int[maxLength];
818     // Ensure comparison selection is correct even when order is reversed.
819     if (reverse) {
820       for (int i = 0; i < baseLength; i++) {
821         double minValue = Double.MAX_VALUE;
822         for (int j = 0; j < targetLength; j++) {
823           if (minValue > minDiffs[i][j]) {
824             minValue = minDiffs[i][j];
825             baseTargetMap[i] = uniqueTarget[j];
826           }
827         }
828       }
829     } else {
830       for (int i = 0; i < targetLength; i++) {
831         double minValue = Double.MAX_VALUE;
832         for (int j = 0; j < baseLength; j++) {
833           if (minValue > minDiffs[j][i]) {
834             minValue = minDiffs[j][i];
835             baseTargetMap[i] = uniqueBase[j];
836           }
837         }
838       }
839     }
840 
841     // Finish logging/saving if we are done.
842     if (nAU == 1 && !strict) {
843       // If only comparing one entity we are done. Return values.
844       // Rg ranges from 1-->sqrt(3). Normalize to get from 0-->1
845       double baseGyration = radiusOfGyration(bestBaseNAUs[baseBestZ][targetBestZ].clone(), massN);
846       gyrations[0] = baseGyration;
847       double targetGyration = radiusOfGyration(bestTargetNAUs[baseBestZ][targetBestZ].clone(),
848           massN);
849       gyrations[1] = targetGyration;
850       if (useSym) {
851         // We want to log file indices to exclude not compared atoms.
852         final int numAtomsPerMol1 = atoms1.length/z1;
853         final int numAtomsPerMol2 = atoms2.length/z2;
854         // Check for multiple asymmetric units in the crystals
855         boolean multisym = z1 > 1 || z2 > 1;
856         StringBuilder alchemicalAtoms = null;
857         StringBuilder alchemicalAtoms2 = null;
858         StringBuilder separation = new StringBuilder();
859         for (int i = 0; i < z1; i++) {
860           for (int j = 0; j < z2; j++) {
861             separation.append("\n Atom Separation (A)  Description");
862             if (multisym) {
863               separation.append(format(" (Z1'' =%2d Z2'' =%2d)\n  Base: Target:  Distance:\n", i, j));
864             } else {
865               separation.append(" \n");
866             }
867             for (int k = 0; k < compareAtomsSize; k++) {
868               final int index = k * 3;
869               final double value = rmsd(
870                   new double[]{bestBaseNAUs[i][j][index], bestBaseNAUs[i][j][index + 1],
871                       bestBaseNAUs[i][j][index + 2]},
872                   new double[]{bestTargetNAUs[i][j][index], bestTargetNAUs[i][j][index + 1],
873                       bestTargetNAUs[i][j][index + 2]}, massN);
874               if (logger.isLoggable(Level.INFO)) {
875                 if (printSym < value) {
876                   // Collect atomic separation distances
877                   separation.append(
878                       format(" %4d %4d  %14.6f\n", i * numAtomsPerMol1 + comparisonAtoms[k] + 1,
879                           j * numAtomsPerMol2 + comparisonAtoms[k] + 1, value));
880                   // Collect alchemical atoms
881                   if (alchemicalAtoms == null) {
882                     alchemicalAtoms = new StringBuilder();
883                     if (multisym) {
884                       alchemicalAtoms.append(format(" %3d %3d  ", i, j));
885                     }
886                     alchemicalAtoms.append(i * numAtomsPerMol1 + comparisonAtoms[k] + 1);
887                     alchemicalAtoms2 = new StringBuilder();
888                     if (multisym) {
889                       alchemicalAtoms2.append(format(" %3d %3d  ", i, j));
890                     }
891                     alchemicalAtoms2.append(j * numAtomsPerMol2 + comparisonAtoms[k] + 1);
892                   } else if (alchemicalAtoms.charAt(alchemicalAtoms.length() - 1) == '\n') {
893                     alchemicalAtoms.append(format(" %3d %3d  ", i, j))
894                         .append(i * numAtomsPerMol1 + comparisonAtoms[k] + 1);
895                     alchemicalAtoms2.append(format(" %3d %3d  ", i, j))
896                         .append(j * numAtomsPerMol2 + comparisonAtoms[k] + 1);
897                   } else {
898                     alchemicalAtoms.append(",")
899                         .append(i * numAtomsPerMol1 + comparisonAtoms[k] + 1);
900                     alchemicalAtoms2.append(",")
901                         .append(j * numAtomsPerMol2 + comparisonAtoms[k] + 1);
902                   }
903                 }
904               }
905             }
906             if (useSave) {
907               if (machineLearning) {
908                 saveAssembly(file1, name1, bondList1, atoms1, forceField1, bestBaseNAUs[i][j],
909                     comparisonAtoms, nAU, "_c1", 0.000, compNum, saveClusters);
910                 saveAssembly(file2, name2, bondList2, atoms2, forceField2, bestTargetNAUs[i][j],
911                     comparisonAtoms, nAU, "_c2", min1, compNum, saveClusters);
912               } else {
913                 saveAssembly(file1, name1, bondList1, atoms1, forceField1, bestBaseNAUs[i][j],
914                     comparisonAtoms, nAU, "_c1", compNum, saveClusters);
915                 saveAssembly(file2, name2, bondList2, atoms2, forceField2, bestTargetNAUs[i][j],
916                     comparisonAtoms, nAU, "_c2", compNum, saveClusters);
917               }
918             }
919             if (alchemicalAtoms != null) {
920               alchemicalAtoms.append("\n");
921               alchemicalAtoms2.append("\n");
922             }
923           }
924         }
925         if (alchemicalAtoms != null) {
926           stringBuilder.append(separation);
927           stringBuilder.append("\n Suggested Alchemical Atoms:\n");
928           // If more than one entity being compared, specify which atoms belong to which molecule of each crystal.
929           if (multisym) {
930             stringBuilder.append(" Base: (").append(baseLabel).append(")\n");
931           }
932           stringBuilder.append(alchemicalAtoms).append("\n");
933           if (multisym) {
934             stringBuilder.append(" Target: (").append(targetLabel).append(")\n")
935                 .append(alchemicalAtoms2).append("\n");
936           }
937         }
938         stringBuilder.append(format(" Max RMSD: %9.4f B Index: %d T Index: %d", max1, maxBIndex, maxTIndex));
939       }
940       if (useSave && !useSym) {
941         if (machineLearning) {
942           saveAssembly(file1, name1, bondList1, atoms1, forceField1,
943               bestBaseNAUs[baseBestZ][targetBestZ], comparisonAtoms, nAU, "_c1", 0.000, compNum,
944               saveClusters);
945           saveAssembly(file2, name2, bondList2, atoms2, forceField2,
946               bestTargetNAUs[baseBestZ][targetBestZ], comparisonAtoms, nAU, "_c2", min1, compNum,
947               saveClusters);
948         } else {
949           saveAssembly(file1, name1, bondList1, atoms1, forceField1,
950               bestBaseNAUs[baseBestZ][targetBestZ], comparisonAtoms, nAU, "_c1", compNum, saveClusters);
951           saveAssembly(file2, name2, bondList2, atoms2, forceField2,
952               bestTargetNAUs[baseBestZ][targetBestZ], comparisonAtoms, nAU, "_c2", compNum, saveClusters);
953         }
954       }
955       if (logger.isLoggable(Level.FINER) && useSym) {
956         for (int i = 0; i < z1; i++) {
957           for (int j = 0; j < z2; j++) {
958             printSym(compareAtomsSize, file2, name2, bondList2, atoms2, forceField2, saveClusters, j);
959           }
960         }
961       }
962       return min1;
963     }
964     if (logger.isLoggable(Level.FINER)) {
965       stringBuilder.append(
966           " Minimum RMSD_1 Between Unique Base and Target AUs:\n i  j (bInd tInd) RMSD_1\n");
967       for (int i = 0; i < baseLength; i++) {
968         for (int j = 0; j < targetLength; j++) {
969           stringBuilder.append(
970               format(" %d %d (%4d %4d) %4.4f\n", i, j, baseAUDist[uniqueBase[i]].index(),
971                   targetAUDist[uniqueTarget[j]].index(), minDiffs[i][j]));
972         }
973       }
974     }
975 
976     // Coordinate arrays to save out structures at the end.
977     double bestRMSD = Double.MAX_VALUE;
978     // Reset values from minZdiffs... or allocate another array.
979     for (double[] row : minZdiffs) {
980       fill(row, Double.MAX_VALUE);
981     }
982     if (logger.isLoggable(Level.FINE)) {
983       stringBuilder.append(
984           format("\n  Trial     RMSD_1 (%8s)  RMSD_3 (%8s)  %8s  G(r1)    G(r2)\n", rmsdLabel,
985               rmsdLabel, rmsdLabel));
986     }
987     baseAUDist_2 = new DoubleIndexPair[nBaseMols];
988     targetAUDist_2 = new DoubleIndexPair[nTargetMols];
989     // Begin comparison
990     // Integer used only for user display logging.
991     int currentComparison = 1;
992     for (int l = 0; l < baseLength; l++) {
993       final int bIndex = uniqueBase[l];
994       final int baseInd = baseAUDist[bIndex].index();
995       final int currZ1 = baseDistMap.get(baseInd) % z1;
996 
997       if (l > 0) {
998         // Reset coords for next comparison.
999         arraycopy(baseXYZoriginal, 0, baseXYZ, 0, baseXYZoriginal.length);
1000       }
1001       //Re-prioritize based on center-most molecule
1002       prioritizeReplicates(baseXYZ, baseCoM[0], compareAtomsSize, baseAUDist_2,
1003           baseInd, linkage);
1004       //Translate base system based on center-most molecule
1005       int baseAUIndex = baseAUDist_2[0].index() * nCoords;
1006       arraycopy(baseXYZ, baseAUIndex, baseAU, 0, nCoords);
1007       for (int i = 0; i < 4; i++) {
1008         for (int j = 0; j < nAU; j++) {
1009           final int molIndex = baseAUDist_2[j].index() * nCoords;
1010           arraycopy(baseXYZ, molIndex, baseNAUs[i], j * nCoords, nCoords);
1011         }
1012       }
1013       // Translate base to the origin
1014       double[] translation = calculateTranslation(baseAU, massN);
1015       applyTranslation(baseAU, translation);
1016       applyTranslation(baseNAUs[1], translation);
1017       applyTranslation(baseNAUs[2], translation);
1018       applyTranslation(baseNAUs[3], translation);
1019       applyTranslation(baseXYZ, translation);
1020       //Update CoMs with translation
1021       centerOfMass(baseCoM[1], baseXYZ, massN, massSum, compareAtomsSize);
1022       if (useSym) {
1023         // Create new identity SymOp for comparison.
1024         baseTransformSymOp = new SymOp(ZERO_ROTATION, Tr_0_0_0);
1025         // All target comparisons are performed based on the same base AU.
1026         final SymOp base = baseSymOps.get(baseInd);
1027         baseSymOp = new SymOp(base.rot, base.tr);
1028         baseTransformSymOp = baseTransformSymOp.append(new SymOp(ZERO_ROTATION, translation));
1029         if (logger.isLoggable(Level.FINEST)) {
1030           stringBuilder.append(format("\n Base %d (%2d) to Origin Translation: ", l, bIndex))
1031               .append(Arrays.toString(translation)).append("\n");
1032         }
1033       }
1034 
1035       // Acquire coordinates based on center 3 molecules
1036       if (logger.isLoggable(Level.FINEST)) {
1037         stringBuilder.append("\n Base 3 Conformations:\n");
1038       }
1039       for (int i = 0; i < 3; i++) {
1040         int baseIndex = baseAUDist_2[i].index() * nCoords;
1041         arraycopy(baseXYZ, baseIndex, base3AUs, i * nCoords, nCoords);
1042       }
1043       translation = calculateTranslation(base3AUs, massN);
1044       applyTranslation(base3AUs, translation);
1045       applyTranslation(baseNAUs[2], translation);
1046       applyTranslation(baseNAUs[3], translation);
1047       applyTranslation(baseXYZ, translation);
1048       //Update CoMs with translation
1049       centerOfMass(baseCoM[2], baseXYZ, massN, massSum, compareAtomsSize);
1050       if (useSym) {
1051         baseTransformSymOp = baseTransformSymOp.append(new SymOp(ZERO_ROTATION, translation));
1052         if (logger.isLoggable(Level.FINEST)) {
1053           stringBuilder.append(format("\n Base %d (%2d) 2nd Translation: ", l, bIndex))
1054               .append(Arrays.toString(translation)).append("\n");
1055         }
1056       }
1057 
1058       // Acquire coordinates for final comparison
1059       final double maxDist = baseAUDist[baseAUDist.length - 1].doubleValue();
1060       if (logger.isLoggable(Level.FINEST)) {
1061         stringBuilder.append(format("\n System 1 farthest distance: %4.8f", sqrt(maxDist)));
1062       }
1063       for (int i = 0; i < nAU; i++) {
1064         final int molIndex = baseAUDist_2[i].index() * nCoords;
1065         arraycopy(baseXYZ, molIndex, baseNAUs[3], i * nCoords, nCoords);
1066       }
1067       translation = calculateTranslation(baseNAUs[3], massN);
1068       // array copy to baseNAUs
1069       applyTranslation(baseNAUs[3], translation);
1070       applyTranslation(baseXYZ, translation);
1071       if (useSym) {
1072         baseTransformSymOp = baseTransformSymOp.append(new SymOp(ZERO_ROTATION, translation));
1073         if (logger.isLoggable(Level.FINEST)) {
1074           stringBuilder.append(format("\n Base %d (%2d) Final Translation: ", l, bIndex))
1075               .append(Arrays.toString(translation)).append("\n");
1076         }
1077       }
1078 
1079       for (int m = 0; m < targetLength; m++) {
1080         final int tIndex = uniqueTarget[m];
1081         final int targetInd = targetAUDist[tIndex].index();
1082         final int currZ2 = targetDistMap.get(targetInd) % z2;
1083         if (strict || !reverse && baseTargetMap[m] == bIndex
1084             || reverse && baseTargetMap[l] == tIndex) {
1085 
1086           arraycopy(targetXYZoriginal, 0, targetXYZ, 0, targetXYZoriginal.length);
1087 
1088           //Re-prioritize based on central AU if different from first prioritization.
1089           prioritizeReplicates(targetXYZ, targetCoM, compareAtomsSize,
1090               targetAUDist_2, targetInd, linkage);
1091 
1092           // Isolate the AU closest to center in the second crystal (targetAU).
1093           arraycopy(targetXYZ, targetAUDist_2[0].index() * nCoords, targetAU, 0, nCoords);
1094 
1095           // Translate target to origin
1096           translation = calculateTranslation(targetAU, massN);
1097           // Move the center current AU
1098           applyTranslation(targetAU, translation);
1099           // Move the entire system
1100           applyTranslation(targetXYZ, translation);
1101 
1102           // Rotate the target molecule onto the base molecule.
1103           double[][] rotation = calculateRotation(baseAU, targetAU, massN);
1104           applyRotation(targetAU, rotation);
1105           applyRotation(targetXYZ, rotation);
1106           //Update center of masses with the first trans/rot
1107           centerOfMass(targetCoM, targetXYZ, massN, massSum, compareAtomsSize);
1108 
1109           if (useSym) {
1110             // Reset values for printing symmetry operations
1111             targetTransformSymOp = new SymOp(ZERO_ROTATION, Tr_0_0_0);
1112             // Switch m center most molecules (looking for stereoisomers)
1113             final SymOp target = targetSymOps.get(targetInd);
1114             targetSymOp = new SymOp(target.rot, target.tr);
1115             //Matrix order matters. Perform translation then rotation. Standard append applies rotation then translation.
1116 //            targetTransformSymOp[currZ1][currZ2] = targetTransformSymOp[currZ1][currZ2].append(new SymOp(rotation, Tr_0_0_0));
1117             targetTransformSymOp = targetTransformSymOp.append(new SymOp(ZERO_ROTATION, translation))
1118                 .append(new SymOp(rotation, Tr_0_0_0));
1119 
1120             if (logger.isLoggable(Level.FINEST)) {
1121               stringBuilder.append(matrixToString(baseSymOp.rot, bIndex, "Base Sym Rot"));
1122               stringBuilder.append(format("\n Base %d Sym Op Translation: ", bIndex))
1123                   .append(Arrays.toString(baseSymOp.tr)).append("\n");
1124               stringBuilder.append(matrixToString(targetSymOp.rot, tIndex, "Target Sym Rot"));
1125               stringBuilder.append(format("\n Target %d Sym Op Translation: ", tIndex))
1126                   .append(Arrays.toString(targetSymOp.tr)).append("\n");
1127               stringBuilder.append(format("\n Trans target %d (%2d) sys to origin: \n\t", m, tIndex))
1128                   .append(Arrays.toString(translation)).append("\n");
1129               stringBuilder.append(matrixToString(rotation, tIndex, "1st Rot"));
1130             }
1131           }
1132 
1133           // At this point both systems have completed first rotation/translation
1134           //  Therefore both center-most molecules should be overlapped.
1135           // Need 3 for progressive alignment even if RMSD_1 or RMSD_2 is desired
1136           pairEntities(max(3, nAU), 1);
1137 
1138           double checkRMSD1 = -3.0;
1139           double n1RMSD = -4.0;
1140           if (logger.isLoggable(Level.FINE)) {
1141             checkRMSD1 = rmsd(baseAU, targetAU, massN);
1142             for (int i = 0; i < nAU; i++) {
1143               final int offset = i * nCoords;
1144               final int molIndex = pairedAUs[i].index() * nCoords;
1145               arraycopy(targetXYZ, molIndex, targetNAUs, offset, nCoords);
1146             }
1147             n1RMSD = rmsd(baseNAUs[1], targetNAUs, massN);
1148             if (logger.isLoggable(Level.FINEST) && useSave) {
1149               saveAssembly(file1, name1, bondList1, atoms1, forceField1, baseAU, comparisonAtoms, 1,
1150                   "_c1_1", compNum, saveClusters);
1151               saveAssembly(file1, name1, bondList1, atoms1, forceField1, baseNAUs[1],
1152                   comparisonAtoms, nAU, "_c1_1N", compNum, saveClusters);
1153               saveAssembly(file2, name2, bondList2, atoms2, forceField2, targetAU, comparisonAtoms,
1154                   1, "_c2_1", compNum, saveClusters);
1155               saveAssembly(file2, name2, bondList2, atoms2, forceField2, targetNAUs, comparisonAtoms,
1156                   nAU, "_c2_1N", compNum, saveClusters);
1157             }
1158           }
1159 
1160           // Load coordinates for 3 molecules for the target systems
1161           for (int i = 0; i < 3; i++) {
1162             final int targetIndex = pairedAUs[i].index() * nCoords;
1163             final int coordIndex = i * nCoords;
1164             arraycopy(targetXYZ, targetIndex, target3AUs, coordIndex, nCoords);
1165           }
1166 
1167           translation = calculateTranslation(target3AUs, massN);
1168           applyTranslation(target3AUs, translation);
1169           applyTranslation(targetNAUs, translation);
1170           applyTranslation(targetXYZ, translation);
1171 
1172           // Perform 2nd Rotation
1173           // Calculate the rotation matrix and apply it to the target system.
1174           rotation = calculateRotation(base3AUs, target3AUs, massN);
1175           applyRotation(target3AUs, rotation);
1176           applyRotation(targetNAUs, rotation);
1177           applyRotation(targetXYZ, rotation);
1178           // Update center of masses with the second trans/rot.
1179           centerOfMass(targetCoM, targetXYZ, massN, massSum, compareAtomsSize);
1180           if (useSym) {
1181             applyTranslation(targetAU, translation);
1182             applyRotation(targetAU, rotation);
1183             targetTransformSymOp = targetTransformSymOp.append(new SymOp(ZERO_ROTATION, translation))
1184                 .append(new SymOp(rotation, Tr_0_0_0));
1185             if (logger.isLoggable(Level.FINEST)) {
1186               stringBuilder.append(
1187                       format("\n Target %d (%2d) 2nd Translation/Rotation: ", m, tIndex))
1188                   .append(Arrays.toString(translation)).append("\n");
1189               printSym(compareAtomsSize, file2, name2, bondList2, atoms2, forceField2, saveClusters, currZ2);
1190             }
1191           }
1192 
1193           double checkRMSD2 = -5.0;
1194           double n3RMSD = -6.0;
1195           if (logger.isLoggable(Level.FINE)) {
1196             checkRMSD2 = rmsd(base3AUs, target3AUs, massN);
1197             n3RMSD = rmsd(baseNAUs[2], targetNAUs, massN);
1198             if (logger.isLoggable(Level.FINEST) && useSave) {
1199               saveAssembly(file1, name1, bondList1, atoms1, forceField1, base3AUs, comparisonAtoms,
1200                   3, "_c1_3", compNum, saveClusters);
1201               saveAssembly(file1, name1, bondList1, atoms1, forceField1, baseNAUs[2],
1202                   comparisonAtoms, nAU, "_c1_3N", compNum, saveClusters);
1203               saveAssembly(file2, name2, bondList2, atoms2, forceField2, target3AUs, comparisonAtoms,
1204                   3, "_c2_3", compNum, saveClusters);
1205               saveAssembly(file2, name2, bondList2, atoms2, forceField2, targetNAUs, comparisonAtoms,
1206                   nAU, "_c2_3N", compNum, saveClusters);
1207             }
1208           }
1209 
1210           // Rotations 1 and 2 have been completed and both systems should be overlapped
1211           //  Isolate center most nAU from System 1 and matching molecules from System 2
1212           pairEntities(nAU, 2);
1213 
1214           for (int i = 0; i < nAU; i++) {
1215             final int offset = i * nCoords;
1216             final int molIndex = pairedAUs[i].index() * nCoords;
1217             arraycopy(targetXYZ, molIndex, targetNAUs, offset, nCoords);
1218           }
1219 
1220           translation = calculateTranslation(targetNAUs, massN);
1221           applyTranslation(targetNAUs, translation);
1222           applyTranslation(targetXYZ, translation);
1223           rotation = calculateRotation(baseNAUs[3], targetNAUs, massN);
1224           if (useSym) {
1225             applyTranslation(targetAU, translation);
1226             applyRotation(targetAU, rotation);
1227             targetTransformSymOp = targetTransformSymOp.append(new SymOp(ZERO_ROTATION, translation))
1228                 .append(new SymOp(rotation, Tr_0_0_0));
1229 //            targetTransformSymOp[currZ1][currZ2] = targetTransformSymOp[currZ1][currZ2].append(new SymOp(rotation, Tr_0_0_0));
1230             if (logger.isLoggable(Level.FINEST)) {
1231               stringBuilder.append(matrixToString(rotation, bIndex, "Target System Final Rotation"));
1232               stringBuilder.append(format("\n Target %d Final Translation: ", bIndex))
1233                   .append(Arrays.toString(translation)).append("\n");
1234               printSym(compareAtomsSize, file2, name2, bondList2, atoms2, forceField2, saveClusters, currZ2);
1235             }
1236           }
1237           if (logger.isLoggable(Level.FINEST) && useSave) {
1238             saveAssembly(file1, name1, bondList1, atoms1, forceField1, baseNAUs[3], comparisonAtoms,
1239                 nAU, "_c1_N", compNum, saveClusters);
1240             saveAssembly(file2, name2, bondList2, atoms2, forceField2, targetNAUs, comparisonAtoms,
1241                 nAU, "_c2_N", compNum, saveClusters);
1242           }
1243           applyRotation(targetNAUs, rotation);
1244           applyRotation(targetXYZ, rotation);
1245           final double rmsdSymOp = rmsd(baseNAUs[3], targetNAUs, massN);
1246 
1247           if (logger.isLoggable(Level.FINEST) && useSave) {
1248             saveAssembly(file1, name1, bondList1, atoms1, forceField1, baseNAUs[3], comparisonAtoms,
1249                 nAU, "_c1_N2", compNum, saveClusters);
1250             saveAssembly(file2, name2, bondList2, atoms2, forceField2, targetNAUs, comparisonAtoms,
1251                 nAU, "_c2_N2", compNum, saveClusters);
1252           }
1253 
1254           final double baseGyration = radiusOfGyration(baseNAUs[3], massN);
1255           final double targetGyration = radiusOfGyration(targetNAUs, massN);
1256 
1257           if (logger.isLoggable(Level.FINE)) {
1258             final int totalComparisons = (strict) ? baseLength * targetLength : maxLength;
1259             String output = format(" %2d of %2d: %7.4f (%8.4f) %7.4f (%8.4f) %8.4f %8.4f %8.4f",
1260                 currentComparison, totalComparisons, checkRMSD1, n1RMSD, checkRMSD2, n3RMSD,
1261                 rmsdSymOp, baseGyration, targetGyration);
1262 
1263             if (logger.isLoggable(Level.FINER)) {
1264               if (reverse) {
1265                 output += format(" b: %2d t: %2d bt: %2d", baseAUDist[bIndex].index(),
1266                     targetAUDist[uniqueTarget[m]].index(), targetAUDist[baseTargetMap[m]].index());
1267               } else {
1268                 output += format(" b: %2d t: %2d tb: %2d", baseAUDist[bIndex].index(),
1269                     targetAUDist[uniqueTarget[m]].index(), baseAUDist[baseTargetMap[m]].index());
1270               }
1271             }
1272             stringBuilder.append(output).append("\n");
1273           }
1274 
1275           if (useSym && rmsdSymOp < minZdiffs[currZ1][currZ2]) {
1276             minZdiffs[currZ1][currZ2] = rmsdSymOp;
1277             bestTargetTransformSymOp[currZ1][currZ2] = new SymOp(targetTransformSymOp.rot,
1278                 targetTransformSymOp.tr);
1279             bestTargetSymOp[currZ1][currZ2] = new SymOp(targetSymOp.rot, targetSymOp.tr);
1280             bestBaseSymOp[currZ1][currZ2] = new SymOp(baseSymOp.rot, baseSymOp.tr);
1281             bestBaseTransformSymOp[currZ1][currZ2] = new SymOp(baseTransformSymOp.rot,
1282                 baseTransformSymOp.tr);
1283           }
1284           if (rmsdSymOp < bestRMSD) {
1285             // Rg ranges from 1-->sqrt(3). Normalize to get from 0-->1
1286             gyrations[0] = baseGyration;
1287             gyrations[1] = targetGyration;
1288             bestRMSD = rmsdSymOp;
1289             baseBestZ = currZ1;
1290             targetBestZ = currZ2;
1291             arraycopy(baseNAUs[3], 0, bestBaseNAUs[currZ1][currZ2], 0, nAU * nCoords);
1292             arraycopy(targetNAUs, 0, bestTargetNAUs[currZ1][currZ2], 0, nAU * nCoords);
1293           }
1294           currentComparison++;
1295         }
1296       }
1297     }
1298 
1299     double finalRMSD;
1300     if (bestRMSD < Double.MAX_VALUE) {
1301       finalRMSD = bestRMSD;
1302     } else {
1303       stringBuilder.append(" This RMSD was filtered out! Try the --st flag or increasing --if.\n");
1304       // TODO: Double.NaN causes an error in RunningStatistics... Set to -7.0 for now...
1305       finalRMSD = -7.0;
1306     }
1307     if (nAU == 1 && abs(min1 - finalRMSD) > MATCH_TOLERANCE) {
1308       stringBuilder.append(
1309           format(" WARNING: Single molecule overlay (%8.4f) does not match PAC RMSD_1 (%8.4f)", min1,
1310               finalRMSD));
1311     }
1312     if (useSave) {
1313       if (machineLearning) {
1314         saveAssembly(file1, name1, bondList1, atoms1, forceField1,
1315             bestBaseNAUs[baseBestZ][targetBestZ], comparisonAtoms, nAU, "_c1", 0.000, compNum, saveClusters);
1316         saveAssembly(file2, name2, bondList2, atoms2, forceField2,
1317             bestTargetNAUs[baseBestZ][targetBestZ], comparisonAtoms, nAU, "_c2", finalRMSD, compNum,
1318             saveClusters);
1319       } else {
1320         saveAssembly(file1, name1, bondList1, atoms1, forceField1,
1321             bestBaseNAUs[baseBestZ][targetBestZ], comparisonAtoms, nAU, "_c1", compNum, saveClusters);
1322         saveAssembly(file2, name2, bondList2, atoms2, forceField2,
1323             bestTargetNAUs[baseBestZ][targetBestZ], comparisonAtoms, nAU, "_c2", compNum, saveClusters);
1324       }
1325     }
1326 
1327     if (inertia) {
1328       bestBaseMandV = momentsOfInertia(bestBaseNAUs[baseBestZ][targetBestZ], massN, false, false,
1329           true);
1330       bestTargetMandV = momentsOfInertia(bestTargetNAUs[baseBestZ][targetBestZ], massN, false, false,
1331           true);
1332     }
1333 
1334     if (gyrationComponents) {
1335       bestBaseRg = radiusOfGyrationComponents(bestBaseNAUs[baseBestZ][targetBestZ], massN, true);
1336       bestTargetRg = radiusOfGyrationComponents(bestTargetNAUs[baseBestZ][targetBestZ], massN, true);
1337     }
1338 
1339     return finalRMSD;
1340   }
1341 
1342   /**
1343    * Compare the crystals within the SystemFilters that were inputted into the constructor of this
1344    * class.
1345    *
1346    * @param nAU                Number of asymmetric units to compare.
1347    * @param inflationFactor    Specify safety factor when generating replicates crystal.
1348    * @param matchTol           Tolerance to determine whether two AUs are the same (increases efficiency).
1349    * @param zPrime             Number of asymmetric units in first crystal (default attempts detection).
1350    * @param zPrime2            Number of asymmetric units in second crystal (default attempts detection).
1351    * @param excludeAtomsA      List of atoms specific to first crystal.
1352    * @param excludeAtomsB      List of atoms specific to second crystal.
1353    * @param alphaCarbons       Perform comparisons on only alpha carbons.
1354    * @param includeHydrogen    Perform comparisons without hydrogen atoms.
1355    * @param massWeighted       Perform comparisons with mass weighted coordinates (center of mass
1356    *                           instead of geometric center).
1357    * @param crystalPriority    Prioritize most dense (0), least dense (1), or first inputted file
1358    *                           (2).
1359    * @param strict             More intensive, but less efficient version of PAC.
1360    * @param saveClusters               Save out files of the resulting superposition.
1361    * @param restart            Try to restart from a previous job.
1362    * @param write              Save out a PAC RMSD file.
1363    * @param machineLearning    Save out CSV files for machine learning input (saves PDBs as well).
1364    * @param inertia            Compute moments of inertia for final clusters.
1365    * @param gyrationComponents Compute axial components for radius of gyration of final
1366    *                           clusters.
1367    * @param linkage            Prioritize entities based on single, average, or complete linkage.
1368    * @param printSym           Print final symmetry operator used to superimpose mobile assembly onto
1369    *                           static assembly.
1370    * @param lowMemory          Crystals will be read in as needed (slower performance, but less memory
1371    *                           intensive)
1372    * @param pacFileName        The filename to use.
1373    * @return RunningStatistics Statistics for comparisons performed.
1374    */
1375   public RunningStatistics comparisons(final int nAU, final double inflationFactor, double matchTol, final double hitTol, final int zPrime,
1376                                        final int zPrime2, final String excludeAtomsA, final String excludeAtomsB, final boolean alphaCarbons,
1377                                        final boolean includeHydrogen, final boolean massWeighted, final int crystalPriority, final boolean strict,
1378                                        int saveClusters, final double save, final boolean restart, final boolean write, final boolean machineLearning, boolean inertia,
1379                                        final boolean gyrationComponents, int linkage, final double printSym, boolean lowMemory,
1380                                        final String pacFileName) {
1381     this.printSym = printSym;
1382     //TODO: Incorporate graphic user interface (gui: ffx)
1383     //TODO: Save systems out as original molecule regardless of selection
1384     //TODO: Handle ring flipping or atoms in equivalent positions (mislabeled atoms)
1385     //TODO: Handle Z' > 1 for heterogeneous/co-crystals better.
1386     RunningStatistics runningStatistics;
1387     if (restart) {
1388       runningStatistics = readMatrix(pacFileName);
1389       if (runningStatistics == null) {
1390         runningStatistics = new RunningStatistics();
1391       }
1392     } else {
1393       runningStatistics = new RunningStatistics();
1394       File file = new File(pacFileName);
1395       if (file.exists() && file.delete()) {
1396         logger.info(format(" PAC RMSD file (%s) was deleted.", pacFileName));
1397         logger.info(" To restart from a previous run, use the '-r' flag.");
1398       }
1399     }
1400     // TODO: Fix issue in following comment (multiple bonded info).
1401     // When caching structures, bonded information is applied to assembly multiple times.
1402     if(save >= 0.0){
1403       lowMemory = true;
1404     }
1405 
1406     // restartRow and restartColumn are initialized to zero when this class was constructed.
1407     // They are updated by the "readMatrix" method if a restart is requested.
1408 
1409     // Read ahead to the base starting conformation.
1410     for (int row = 0; row < restartRow; row++) {
1411       baseFilter.readNext(false, false, row + 1 >= restartRow);
1412     }
1413 
1414     MolecularAssembly baseAssembly = baseFilter.getActiveMolecularSystem();
1415 
1416     // Atom arrays from the 1st assembly.
1417     Atom[] atoms1 = baseAssembly.getAtomArray();
1418     final int nAtoms = atoms1.length;
1419 
1420     // Atom arrays from the 2nd assembly.
1421     MolecularAssembly targetAssembly = targetFilter.getActiveMolecularSystem();
1422     Atom[] atoms2 = targetAssembly.getAtomArray();
1423     final int nAtoms2 = atoms2.length;
1424 
1425     // Collect selected atoms.
1426     ArrayList<Integer> unique = new ArrayList<>(parseAtomRanges("Base Assembly", excludeAtomsA, nAtoms));
1427     if (invalidAtomSelection(unique, atoms1, alphaCarbons, includeHydrogen)) {
1428       logger.warning("\n No atoms were selected for the PAC RMSD in first crystal.");
1429       return null;
1430     }
1431     int[] comparisonAtoms = unique.stream().mapToInt(i -> i).toArray();
1432     List<MSNode> bondedEntities = baseAssembly.getAllBondedEntities();
1433     //Determine number of species within asymmetric unit.
1434     //TODO: Handle Z' > 1 for heterogeneous/co-crystals.
1435     int z1;
1436     if(zPrime > 0){
1437       z1 = zPrime;
1438     }else{
1439       z1 = guessZPrime(unique, baseAssembly.getMoleculeNumbers(), bondedEntities.size());
1440     }
1441 
1442     // Collect selected atoms.
1443     unique = new ArrayList<>(parseAtomRanges("target", excludeAtomsB, nAtoms2));
1444     if (invalidAtomSelection(unique, atoms2, alphaCarbons, includeHydrogen)) {
1445       logger.warning("\n No atoms were selected for the PAC RMSD in second crystal.");
1446       return null;
1447     }
1448     int[] comparisonAtoms2 = unique.stream().mapToInt(i -> i).toArray();
1449 
1450     List<MSNode> bondedEntities2 = targetAssembly.getAllBondedEntities();
1451     int z2;
1452     if(zPrime2 > 0){
1453       z2 = zPrime2;
1454     }else{
1455       logger.info(format(" Z2' info: %2d Num Entities %2d water.", bondedEntities.size(), baseAssembly.getWater().size()));
1456       z2 = guessZPrime(unique, targetAssembly.getMoleculeNumbers(), bondedEntities2.size());
1457     }
1458 
1459     // Each ASU contains z * comparisonAtoms species so treat each species individually.
1460     int compareAtomsSize = comparisonAtoms.length;
1461     int compareAtomsSize2 = comparisonAtoms2.length;
1462 //    int compareAtomsSize = Arrays.stream(compareSizeArray).sum();
1463 //    int compareAtomsSize2 = Arrays.stream(compareSizeArray2).sum();
1464     //Determine number of species within asymmetric unit.
1465     //TODO: Handle Z' > 1 for heterogeneous/co-crystals.
1466     if (z1 > 1) {
1467       compareAtomsSize /= z1;
1468     }
1469     if (z2 > 1) {
1470       compareAtomsSize2 /= z2;
1471     }
1472     if(z1 != z2){
1473       logger.warning(format(" Z'1 (%2d) does not equal Z'2 (%2d).", z1, z2));
1474     }
1475 
1476     // When printing Sym Ops it is imperative to find all molecules (decrease matchTol).
1477     final boolean useSym = printSym >= 0.0;
1478     if (useSym && (z1 > 1 || z2 > 1)) {
1479       matchTol = SYM_TOLERANCE;
1480     }
1481     if (useSym && z1 != z2) {
1482       logger.warning(
1483           " Comparisons to determine symmetry should have the same number of molecules in the asymmetric unit.");
1484     }
1485 
1486     Crystal baseCrystal = baseAssembly.getCrystal().getUnitCell();
1487     Crystal targetCrystal = targetFilter.getActiveMolecularSystem().getCrystal().getUnitCell();
1488     // Sohncke groups are non-enantiogenic, so only 1 copy, 2 if mirror exists.
1489     int baseSearchValue = (baseCrystal.spaceGroup.respectsChirality()) ? z1 : 2 * z1;
1490     int targetSearchValue = (targetCrystal.spaceGroup.respectsChirality()) ? z2 : 2 * z2;
1491 
1492     // Number of used coordinates for atoms in one AU.
1493     final int nCoords = compareAtomsSize * 3;
1494     // Remove duplicated atoms from Z' > 1.
1495     if (this.comparisonAtoms == null) {
1496       this.comparisonAtoms = new int[compareAtomsSize];
1497       arraycopy(comparisonAtoms, 0, this.comparisonAtoms, 0, compareAtomsSize);
1498     }
1499 
1500     int massIndex = 0;
1501     final double[] mass = new double[compareAtomsSize];
1502     for (Integer value : this.comparisonAtoms) {
1503       Atom atom = atoms1[value];
1504       double m = atom.getMass();
1505       mass[massIndex++] = (massWeighted) ? m : 1.0;
1506     }
1507 
1508     massIndex = 0;
1509     final double[] mass2 = new double[compareAtomsSize2];
1510     for (Integer value : comparisonAtoms2) {
1511       if (massIndex == compareAtomsSize2) {
1512         //ComparisonAtoms2 contains all indices for the unit cell.
1513         break;
1514       }
1515       Atom atom = atoms2[value];
1516       final double m = atom.getMass();
1517       mass2[massIndex++] = (massWeighted) ? m : 1.0;
1518     }
1519 
1520     if (!Arrays.equals(mass, mass2)) {
1521       logger.warning(" Mass arrays do not match. Check atom ordering or size.");
1522       if (logger.isLoggable(Level.FINER)) {
1523         for (int i = 0; i < compareAtomsSize; i++) {
1524           if (mass[i] != mass2[i]) {
1525             logger.finer(format(" Index: %d Mass 1: %4.4f Mass 2: %4.4f", i, mass[i], mass2[i]));
1526           }
1527         }
1528       }
1529     }
1530 
1531     //Mass of N species
1532     if (massN == null) {
1533       massN = (nAU > 3) ? new double[compareAtomsSize * nAU] : new double[compareAtomsSize * 3];
1534       for (int i = 0; i < nAU; i++) {
1535         arraycopy(mass, 0, massN, i * compareAtomsSize, compareAtomsSize);
1536       }
1537     }
1538     // Sum of all masses for one species.
1539     if (massSum == 0) {
1540       for (int i = 0; i < compareAtomsSize; i++) {
1541         massSum += massN[i];
1542       }
1543     }
1544 
1545     if (machineLearning) {
1546       saveClusters = 1;
1547     }
1548     if (saveClusters > 2) {
1549       saveClusters = 0;
1550       logger.info(" Save flag specified incorrectly (1:PDB; 2:XYZ). Not saving files.");
1551     }
1552 
1553     if (logger.isLoggable(Level.FINER)) {
1554       if (linkage == 0) {
1555         logger.finer(" Single linkage will be used.");
1556       } else if (linkage == 2) {
1557         logger.finer(" Complete linkage will be used.");
1558       } else if (linkage == 1) {
1559         logger.finer(" Average linkage will be used.");
1560       }
1561     }
1562     if (linkage != 1 && linkage != 0 && linkage != 2) {
1563       logger.warning(
1564           "Prioritization method specified incorrectly (--pm {0, 1, 2}). Using default of average linkage.");
1565       linkage = 1;
1566     }
1567 
1568     // Number of atoms included in the PAC RMSD.
1569     if (compareAtomsSize == 0 || compareAtomsSize2 == 0) {
1570       logger.severe(" No atoms were selected for comparison.");
1571       return runningStatistics;
1572     }
1573     logger.info(format("\n %d atoms will be used for the PAC RMSD out of %d in first crystal.",
1574         compareAtomsSize * z1, nAtoms));
1575     logger.info(format(" %d atoms will be used for the PAC RMSD out of %d in second crystal.\n",
1576         compareAtomsSize2 * z2, nAtoms2));
1577 
1578     // Label for logging.
1579     rmsdLabel = format("RMSD_%d", nAU);
1580 
1581     // Minimum amount of time for a single comparison.
1582     double minTime = Double.MAX_VALUE;
1583     double maxTime = -Double.MIN_VALUE;
1584     // Assemblies used for comparisons. Allows ordering.
1585     if (!lowMemory) {
1586       // Store necessary information in arrays for faster access.
1587       int size = (int) ceil((double) targetSize / (double) numProc);
1588       atomCache = new Atom[size][nAtoms2];
1589       fileCache = new File[size];
1590       nameCache = new String[size];
1591       bondCache = new ArrayList<>();
1592       for (int i = 0; i < size; i++) {
1593         bondCache.add(new ArrayList<>());
1594       }
1595       forceFieldCache = new ForceField[size];
1596       crystalCache = new Crystal[size];
1597 
1598       // Cache assemblies needed for inner loop.
1599       for (int column = restartColumn; column < targetSize; column++) {
1600         final int targetRank = column % numProc;
1601         if (targetRank == rank) {
1602           final int assemblyNum = column / numProc;
1603           MolecularAssembly currentAssembly = targetFilter.getActiveMolecularSystem();
1604           Atom[] arrayAtom = currentAssembly.getAtomArray();
1605           final int atomSize = arrayAtom.length;
1606           for (int i = 0; i < atomSize; i++) {
1607             final double[] xyz = new double[3];
1608             arrayAtom[i].getXYZ(xyz);
1609             atomCache[assemblyNum][i] = new Atom(i, arrayAtom[i].getName(),
1610                 arrayAtom[i].getAtomType(), xyz);
1611           }
1612           List<Bond> currentBonds = currentAssembly.getBondList();
1613           List<Bond> currentBondCache = bondCache.get(assemblyNum);
1614           for (Bond b : currentBonds) {
1615             currentBondCache.add(new Bond(b.getAtom(0), b.getAtom(1)));
1616           }
1617           ForceField currentForcefield = currentAssembly.getForceField();
1618           fileCache[assemblyNum] = new File(currentAssembly.getFile().getName());
1619           forceFieldCache[assemblyNum] = new ForceField(currentForcefield.getProperties());
1620           nameCache[assemblyNum] = currentAssembly.getName();
1621           Crystal cXtal = currentAssembly.getCrystal().getUnitCell();
1622           crystalCache[assemblyNum] = new Crystal(cXtal.a, cXtal.b, cXtal.c, cXtal.alpha, cXtal.beta,
1623               cXtal.gamma, cXtal.spaceGroup.pdbName);
1624         }
1625         targetFilter.readNext(false, false, (column + 1) % numProc == rank);
1626       }
1627     }
1628 
1629     if (logger.isLoggable(Level.FINER)) {
1630       logResources();
1631     }
1632 
1633     // Use absolute file path to put it in the same directory as original file.
1634     File saveLocation1 = new File(FilenameUtils.removeExtension(baseAssembly.getFile().getAbsoluteFile().getAbsolutePath()) + "_lt1_" + save + ".arc");
1635     File saveLocation2 = new File(FilenameUtils.removeExtension(targetAssembly.getFile().getAbsoluteFile().getAbsolutePath()) + "_lt2_" + save + ".arc");
1636     if(save >= 0.0) {
1637       logger.info(format(" Saving trajectories less than %7.4f to :\n %s\n %s", save, saveLocation1.getName(), saveLocation2.getName()));
1638     }
1639     // Loop over conformations in the base assembly.
1640     for (int row = restartRow; row < baseSize; row++) {
1641       // Initialize the distance this rank is responsible for to zero.
1642       fill(myDistances, -8.0);
1643       int myIndex = 0;
1644       // Base unit cell for logging.
1645       baseAssembly = baseFilter.getActiveMolecularSystem();
1646       baseCrystal = baseAssembly.getCrystal().getUnitCell();
1647       atoms1 = baseAssembly.getAtomArray();
1648       //Remove atoms not used in comparisons from the original molecular assembly (crystal 1).
1649       final double[] reducedBaseCoords = reduceSystem(atoms1, comparisonAtoms);
1650       final double baseDensity = baseCrystal.getUnitCell().getDensity(baseAssembly.getMass());
1651       if (baseCrystal == null || baseCrystal.aperiodic()) {
1652         logger.warning(" " + baseAssembly.getName() + " does not have a crystal. Consider using Superpose command.\n");
1653         continue;
1654       }
1655 
1656       //Setup for comparison with crystal specific information.
1657       // Density changes based on mass weighted flag, therefore use volume.
1658 
1659       // Estimate a radius that will include desired number of asymmetric units (inflationFactor).
1660       if (logger.isLoggable(Level.FINER)) {
1661         logger.finer(format(" Unit Cell Volume:  (Base) %4.2f (Target) %4.2f", baseCrystal.volume,
1662             targetCrystal.volume));
1663         logger.finer(format(" Unit Cell Symm Ops: (Base) %d (Target) %d", baseCrystal.getNumSymOps(),
1664             targetCrystal.getNumSymOps()));
1665         logger.finer(format(" Z'': (Base) %d (Target) %d", z1, z2));
1666       }
1667 
1668       // When the system was read in, a replicates crystal may have been created to satisfy the cutoff.
1669       // Retrieve a reference to the unit cell (not the replicates crystal).
1670       // Here we will use the unit cell, to create a new replicates crystal that may be
1671       // a different size (i.e. larger).
1672       // Used for LMN specific replicates expansion (add LMN as input to generateInflatedSphere).
1673 //      int[] baseLMN = determineExpansion(baseCrystal.getUnitCell(), reducedBaseCoords, comparisonAtoms, mass, nAU, z1, inflationFactor);
1674       final int[] baseLMN = new int[3];
1675       double inflationFactorOrig = inflationFactor;
1676       baseXYZoriginal = generateInflatedSphere(baseCrystal.getUnitCell(), reducedBaseCoords, z1, nAU,
1677           linkage, mass, baseLMN, strict, baseSymOps, baseDistMap, inflationFactor);
1678       if (inflationFactorOrig != inflationFactor) {
1679         logger.warning(format(
1680             " Default replicates crystal was too small for comparison. Increasing inflation factor from %9.3f to %9.3f",
1681             inflationFactor, inflationFactorOrig));
1682       }
1683       int nBaseCoords = baseXYZoriginal.length;
1684       baseXYZ = new double[nBaseCoords];
1685       arraycopy(baseXYZoriginal, 0, baseXYZ, 0, nBaseCoords);
1686 
1687       // Center of Masses for crystal 1.
1688       int nBaseMols = nBaseCoords / (compareAtomsSize * 3);
1689       if (baseAUDist == null || baseAUDist.length != nBaseMols) {
1690         baseAUDist = new DoubleIndexPair[nBaseMols];
1691       }
1692       if (baseCoM == null || baseCoM.length != nBaseMols) {
1693         baseCoM = new double[3][nBaseMols][3];
1694       }
1695       centerOfMass(baseCoM[0], baseXYZ, massN, massSum, compareAtomsSize);
1696       prioritizeReplicates(baseXYZ, baseCoM[0], compareAtomsSize, baseAUDist, 0,
1697           linkage);
1698 
1699       for (int column = restartColumn; column < targetSize; column++) {
1700         final int targetRank = column % numProc;
1701         if (targetRank == rank) {
1702           stringBuilder.setLength(0);
1703           long time = -System.nanoTime();
1704           final int assemblyNum = column / numProc;
1705           if (!lowMemory) {
1706             targetCrystal = crystalCache[assemblyNum];
1707             atoms2 = atomCache[assemblyNum];
1708           } else {
1709             targetAssembly = targetFilter.getActiveMolecularSystem();
1710             targetCrystal = targetAssembly.getCrystal().getUnitCell();
1711             atoms2 = targetAssembly.getAtomArray();
1712           }
1713           if (targetCrystal == null || targetCrystal.aperiodic()) {
1714             if (!lowMemory) {
1715               logger.warning(" " + nameCache[assemblyNum] + " does not have a crystal. Consider using Superpose command.\n");
1716             } else {
1717               logger.warning(" " + targetAssembly.getName() + " does not have a crystal. Consider using Superpose command.\n");
1718             }
1719             continue;
1720           }
1721           double rmsd;
1722           // Used to determine static/mobile crystal. Needed to switch back after comparison.
1723           boolean densityCheck;
1724           if (isSymmetric && row == column) {
1725             stringBuilder.append(
1726                     format("\n Comparing Model %4d (%s) of %s\n with      Model %4d (%s) of %s\n",
1727                             row + 1, baseCrystal.toShortString(), baseLabel, column + 1,
1728                             targetCrystal.toShortString(), targetLabel));
1729             // Fill the diagonal.
1730             rmsd = 0.0;
1731             // Log the final result.
1732             stringBuilder.append(format("\n PAC %s: %12s %7.4f A\n", rmsdLabel, "", rmsd));
1733           } else if (isSymmetric && row > column) {
1734             // Do not compute lower triangle values.
1735             rmsd = -10.0;
1736           } else {
1737             stringBuilder.append(
1738                     format("\n Comparing Model %4d (%s) of %s\n with      Model %4d (%s) of %s\n",
1739                             row + 1, baseCrystal.toShortString(), baseLabel, column + 1,
1740                             targetCrystal.toShortString(), targetLabel));
1741 
1742             //Remove atoms not used in comparisons from the original molecular assembly (crystal 2).
1743             final double[] reducedTargetCoords = reduceSystem(atoms2, comparisonAtoms2);
1744             // Used for LMN specific replicates expansion (add LMN as input to generateInflatedSphere).
1745 //            int[] targetLMN = determineExpansion(targetCrystal.getUnitCell(), reducedTargetCoords, comparisonAtoms2,
1746 //                mass2, nAU, z2, inflationFactor);
1747             final int[] targetLMN = new int[3];
1748             inflationFactorOrig = inflationFactor;
1749             targetXYZoriginal = generateInflatedSphere(targetCrystal.getUnitCell(),
1750                     reducedTargetCoords, z2, nAU, linkage, mass2, targetLMN, strict, targetSymOps,
1751                     targetDistMap, inflationFactor);
1752             if (inflationFactorOrig != inflationFactor) {
1753               logger.warning(format(
1754                       " Default replicates crystal was too small for comparison. Increasing inflation factor from %9.3f to %9.3f",
1755                       inflationFactor, inflationFactorOrig));
1756             }
1757             int nTargetCoords = targetXYZoriginal.length;
1758             targetXYZ = new double[nTargetCoords];
1759             arraycopy(targetXYZoriginal, 0, targetXYZ, 0, nTargetCoords);
1760 
1761             // Prioritize crystal order based on user specification (High/low density or file order).
1762             double densityMass = 0;
1763             if (!lowMemory) {
1764               for (Atom atom : atomCache[assemblyNum]) {
1765                 densityMass += atom.getMass();
1766               }
1767             } else {
1768               densityMass = targetFilter.getActiveMolecularSystem().getMass();
1769             }
1770             final double targetDensity = targetCrystal.getUnitCell().getDensity(densityMass);
1771             if (logger.isLoggable(Level.FINER)) {
1772               stringBuilder.append(
1773                       format("\n Base Density: %4.4f Target Density: %4.4f\n", baseDensity,
1774                               targetDensity));
1775             }
1776             // Using low density as a base can result in poor overlap (think Nyquist)
1777             densityCheck =
1778                     (crystalPriority == 1) ? baseDensity < targetDensity : baseDensity > targetDensity;
1779 
1780             // Center of Masses for crystal 1.
1781             int nTargetMols = targetXYZ.length / (compareAtomsSize2 * 3);
1782             if (targetAUDist == null || targetAUDist.length != nTargetMols) {
1783               targetAUDist = new DoubleIndexPair[nTargetMols];
1784             }
1785             if (targetCoM == null || targetCoM.length != nTargetMols) {
1786               targetCoM = new double[nTargetMols][3];
1787             }
1788             //Update center of mass and priority for each target crystal..
1789             centerOfMass(targetCoM, targetXYZ, massN, massSum, compareAtomsSize);
1790             prioritizeReplicates(targetXYZ, targetCoM, compareAtomsSize,
1791                     targetAUDist, 0, linkage);
1792 
1793             // Flip system order based on crystalPriority if needed.
1794             File file1;
1795             File file2;
1796             String name1;
1797             String name2;
1798             List<Bond> bondList1;
1799             List<Bond> bondList2;
1800             ForceField forceField1;
1801             ForceField forceField2;
1802             arraycopy(baseXYZoriginal, 0, baseXYZ, 0, nBaseCoords);
1803             if (densityCheck || crystalPriority == 2) {
1804               // Keep file ordering
1805               atoms1 = baseAssembly.getAtomArray();
1806               file1 = baseAssembly.getFile();
1807               name1 = baseAssembly.getName();
1808               bondList1 = baseAssembly.getBondList();
1809               forceField1 = baseAssembly.getForceField();
1810               if (!lowMemory) {
1811                 atoms2 = atomCache[assemblyNum];
1812                 file2 = fileCache[assemblyNum];
1813                 name2 = nameCache[assemblyNum];
1814                 bondList2 = bondCache.get(assemblyNum);
1815                 forceField2 = forceFieldCache[assemblyNum];
1816               } else {
1817                 MolecularAssembly mobileAssembly = targetFilter.getActiveMolecularSystem();
1818                 atoms2 = mobileAssembly.getAtomArray();
1819                 file2 = mobileAssembly.getFile();
1820                 name2 = mobileAssembly.getName();
1821                 bondList2 = mobileAssembly.getBondList();
1822                 forceField2 = mobileAssembly.getForceField();
1823               }
1824             } else {
1825               // Swap base and target systems.
1826               double[] tempXYZ = new double[nBaseCoords];
1827               arraycopy(baseXYZ, 0, tempXYZ, 0, nBaseCoords);
1828               baseXYZ = new double[nTargetCoords];
1829               arraycopy(targetXYZ, 0, baseXYZ, 0, nTargetCoords);
1830               targetXYZ = new double[nBaseCoords];
1831               arraycopy(tempXYZ, 0, targetXYZ, 0, nBaseCoords);
1832               arraycopy(baseXYZoriginal, 0, tempXYZ, 0, nBaseCoords);
1833               baseXYZoriginal = new double[nTargetCoords];
1834               arraycopy(targetXYZoriginal, 0, baseXYZoriginal, 0, nTargetCoords);
1835               targetXYZoriginal = new double[nBaseCoords];
1836               arraycopy(tempXYZ, 0, targetXYZoriginal, 0, nBaseCoords);
1837               tempXYZ = new double[nCoords];
1838               arraycopy(reducedBaseCoords, 0, tempXYZ, 0, nCoords);
1839               arraycopy(reducedTargetCoords, 0, reducedBaseCoords, 0, nCoords);
1840               arraycopy(tempXYZ, 0, reducedTargetCoords, 0, nCoords);
1841               ArrayList<SymOp> also = new ArrayList<>(baseSymOps);
1842               baseSymOps = new ArrayList<>(targetSymOps);
1843               targetSymOps = new ArrayList<>(also);
1844               ArrayList<Integer> ali = new ArrayList<>(baseDistMap);
1845               baseDistMap = new ArrayList<>(targetDistMap);
1846               targetDistMap = new ArrayList<>(ali);
1847               DoubleIndexPair[] tempDIP = new DoubleIndexPair[nBaseMols];
1848               arraycopy(baseAUDist, 0, tempDIP, 0, nBaseMols);
1849               baseAUDist = new DoubleIndexPair[nTargetMols];
1850               arraycopy(targetAUDist, 0, baseAUDist, 0, nTargetMols);
1851               targetAUDist = new DoubleIndexPair[nBaseMols];
1852               arraycopy(tempDIP, 0, targetAUDist, 0, nBaseMols);
1853               double[][] tempDD = new double[nBaseMols][3];
1854               arraycopy(baseCoM[0], 0, tempDD, 0, nBaseMols);
1855               baseCoM = new double[3][nTargetMols][3];
1856               arraycopy(targetCoM, 0, baseCoM[0], 0, nTargetMols);
1857               targetCoM = new double[nBaseMols][3];
1858               arraycopy(tempDD, 0, targetCoM, 0, nBaseMols);
1859               atoms2 = baseAssembly.getAtomArray();
1860               file2 = baseAssembly.getFile();
1861               name2 = baseAssembly.getName();
1862               bondList2 = baseAssembly.getBondList();
1863               forceField2 = baseAssembly.getForceField();
1864               int[] tempAtoms = comparisonAtoms.clone();
1865               comparisonAtoms = comparisonAtoms2.clone();
1866               comparisonAtoms2 = tempAtoms.clone();
1867               int temp = z1;
1868               z1 = z2;
1869               z2 = temp;
1870               temp = baseSearchValue;
1871               baseSearchValue = targetSearchValue;
1872               targetSearchValue = temp;
1873               temp = nBaseCoords;
1874               nBaseCoords = nTargetCoords;
1875               nTargetCoords = temp;
1876               temp = nBaseMols;
1877               nBaseMols = nTargetMols;
1878               nTargetMols = temp;
1879               if (!lowMemory) {
1880                 atoms1 = atomCache[assemblyNum];
1881                 file1 = fileCache[assemblyNum];
1882                 name1 = nameCache[assemblyNum];
1883                 bondList1 = bondCache.get(assemblyNum);
1884                 forceField1 = forceFieldCache[assemblyNum];
1885               } else {
1886                 MolecularAssembly staticAssembly = targetFilter.getActiveMolecularSystem();
1887                 atoms1 = staticAssembly.getAtomArray();
1888                 file1 = staticAssembly.getFile();
1889                 name1 = staticAssembly.getName();
1890                 bondList1 = staticAssembly.getBondList();
1891                 forceField1 = staticAssembly.getForceField();
1892               }
1893             }
1894             // Now that Base and Target structures are finalized, allocate variables if necessary.
1895             if (baseAU == null) {
1896               baseAU = new double[nCoords];
1897             }
1898             if (targetAU == null) {
1899               targetAU = new double[nCoords];
1900             }
1901             if (pairedAUs == null || pairedAUs.length != max(3, nAU)) {
1902               pairedAUs = new DoubleIndexPair[max(3, nAU)];
1903             }
1904             if (base3AUs == null) {
1905               base3AUs = new double[nCoords * 3];
1906             }
1907             if (target3AUs == null) {
1908               target3AUs = new double[nCoords * 3];
1909             }
1910             if (baseNAUs == null) {
1911               baseNAUs = new double[4][nAU * nCoords];
1912             }
1913             if (targetNAUs == null) {
1914               targetNAUs = new double[nAU * nCoords];
1915             }
1916             if (bestBaseNAUs == null) {
1917               bestBaseNAUs = new double[z1][z2][nAU * nCoords];
1918             }
1919             if (bestTargetNAUs == null) {
1920               bestTargetNAUs = new double[z1][z2][nAU * nCoords];
1921             }
1922             if (useSym) {
1923               if (baseSymOps == null) {
1924                 baseSymOps = new ArrayList<>();
1925               }
1926               if (targetSymOps == null) {
1927                 targetSymOps = new ArrayList<>();
1928               }
1929               if (baseAUoriginal == null || baseAUoriginal.length != z1) {
1930                 baseAUoriginal = new double[z1][nCoords];
1931               }
1932               if (targetAUoriginal == null || targetAUoriginal.length != z2) {
1933                 targetAUoriginal = new double[z2][nCoords];
1934               }
1935             }
1936 
1937             if (useSym) {
1938               for (int i = 0; i < z1; i++) {
1939                 arraycopy(reducedBaseCoords, i * compareAtomsSize * 3, baseAUoriginal[i], 0,
1940                         compareAtomsSize * 3);
1941               }
1942               for (int i = 0; i < z2; i++) {
1943                 arraycopy(reducedTargetCoords, i * compareAtomsSize2 * 3, targetAUoriginal[i], 0,
1944                         compareAtomsSize2 * 3);
1945               }
1946             }
1947 
1948             if (logger.isLoggable(Level.FINER)) {
1949               logger.finer(
1950                       format(" %3d Molecules in Base Crystal \t %3d Molecules in Target Crystal",
1951                               (nBaseCoords / 3) / compareAtomsSize, (nTargetCoords / 3) / compareAtomsSize));
1952             }
1953 
1954             if (logger.isLoggable(Level.FINEST) && saveClusters > 0) {
1955               saveAssembly(file1, name1, bondList1, atoms1, forceField1, baseXYZoriginal,
1956                       comparisonAtoms, nBaseCoords / 3, "_c1_rep", -1, saveClusters);
1957               saveAssembly(file2, name2, bondList2, atoms2, forceField2, targetXYZoriginal,
1958                       comparisonAtoms2, nTargetCoords / 3, "_c2_rep", -1, saveClusters);
1959             }
1960             // Compute the PAC RMSD.
1961             rmsd = compare(file1, name1, bondList1, atoms1, forceField1, file2, name2, bondList2,
1962                     atoms2, forceField2, z1, z2, compareAtomsSize, nAU, baseSearchValue,
1963                     targetSearchValue, matchTol, row * targetSize + column, strict, saveClusters,
1964                     machineLearning, linkage, inertia, gyrationComponents);
1965             time += System.nanoTime();
1966             final double timeSec = time * 1.0e-9;
1967             // Record the fastest comparison.
1968             if (timeSec < minTime) {
1969               minTime = timeSec;
1970             }
1971             if (timeSec > maxTime) {
1972               maxTime = timeSec;
1973             }
1974             // Log the final result.
1975             final double avgGyration = (gyrations[0] + gyrations[1]) / 2;
1976             final double diff = max(gyrations[0], gyrations[1]) - avgGyration;
1977             stringBuilder.append(
1978                     format("\n PAC %7s: %12s %7.4f A (%5.3f sec) G(r) %7.4f A +/- %7.4f\n", rmsdLabel,
1979                             "", rmsd, timeSec, avgGyration, diff));
1980             if (inertia) {
1981               stringBuilder.append("""
1982 
1983                        Moments of Inertia and Principle Axes for first crystal:
1984                         Moments (amu Ang^2): \t X-, Y-, and Z-Components of Axes:
1985                       """);
1986               for (int i = 1; i < 4; i++) {
1987                 stringBuilder.append(
1988                         format("  %16.3f %12.6f %12.6f %12.6f\n", bestBaseMandV[i - 1][0],
1989                                 bestBaseMandV[0][i], bestBaseMandV[1][i], bestBaseMandV[2][i]));
1990               }
1991               stringBuilder.append("""
1992                        Moments of Inertia and Principle Axes for second crystal:
1993                         Moments (amu Ang^2): \t X-, Y-, and Z-Components of Axes:
1994                       """);
1995               for (int i = 1; i < 4; i++) {
1996                 stringBuilder.append(
1997                         format("  %16.3f %12.6f %12.6f %12.6f\n", bestTargetMandV[i - 1][0],
1998                                 bestTargetMandV[0][i], bestTargetMandV[1][i], bestTargetMandV[2][i]));
1999               }
2000             }
2001             if (gyrationComponents) {
2002               // Crystal 1 metrics
2003               double Rmax = max(max(bestBaseRg[0], bestBaseRg[1]), bestBaseRg[2]);
2004               Rmax *= Rmax;
2005               double Rmed = max(min(bestBaseRg[0], bestBaseRg[1]), bestBaseRg[2]);
2006               Rmed *= Rmed;
2007               double Rmin = min(min(bestBaseRg[0], bestBaseRg[1]), bestBaseRg[2]);
2008               Rmin *= Rmin;
2009               double Rg = Rmax + Rmed + Rmin;
2010               // 0.0 indicates a perfect sphere, and 1.0 indicates all atoms along a single axis.
2011               double asphericity = Rmax - 0.5 * (Rmin + Rmed);
2012               double acylindricity = Rmed - Rmin;
2013               double anisotropy = (asphericity * asphericity + 0.75 * acylindricity * acylindricity);
2014               double totalMass = Arrays.stream(mass).sum();
2015               // Density changes based on mass weighted flag.
2016               double volume = totalMass / max(baseCrystal.getDensity(massSum),
2017                       targetCrystal.getDensity(massSum));
2018               double targetRadius = cbrt(0.75 / PI * volume);
2019               stringBuilder.append(format(" Base Radius Metric: %7.4f", avgGyration / targetRadius));
2020               if (logger.isLoggable(Level.FINER)) {
2021                 stringBuilder.append(
2022                         format("\n Target Base Radius:           %9.3f Ã…\n", targetRadius));
2023                 stringBuilder.append(
2024                         format(" Asphericity:   (%6.3f Ã…^2) %9.3f\n", asphericity, asphericity / Rmax));
2025                 stringBuilder.append(format(" Acylindricity: (%6.3f Ã…^2) %9.3f\n", acylindricity,
2026                         acylindricity / Rmax));
2027                 stringBuilder.append(
2028                         format(" Anisotropy:    (%6.3f Ã…) %9.3f\n", sqrt(sqrt(anisotropy)),
2029                                 anisotropy / (Rg * Rg)));
2030                 stringBuilder.append(
2031                         format("  Ryz %9.3fÃ…  Rxz %9.3fÃ…  Rxy %9.3fÃ…\n", bestBaseRg[0], bestBaseRg[1],
2032                                 bestBaseRg[2]));
2033               }
2034               // Crystal 2 metrics
2035               Rmax = max(max(bestTargetRg[0], bestTargetRg[1]), bestTargetRg[2]);
2036               Rmax *= Rmax;
2037               Rmed = max(min(bestTargetRg[0], bestTargetRg[1]), bestTargetRg[2]);
2038               Rmed *= Rmed;
2039               Rmin = min(min(bestTargetRg[0], bestTargetRg[1]), bestTargetRg[2]);
2040               Rmin *= Rmin;
2041               Rg = Rmax + Rmed + Rmin;
2042               // 0.0 indicates a perfect sphere, and 1.0 indicates all atoms along a single axis.
2043               asphericity = Rmax - 0.5 * (Rmin + Rmed);
2044               acylindricity = Rmed - Rmin;
2045               anisotropy = (asphericity * asphericity + 0.75 * acylindricity * acylindricity);
2046               totalMass = Arrays.stream(mass).sum();
2047               volume = totalMass / targetDensity;
2048               targetRadius = cbrt(0.75 / PI * volume);
2049               if (logger.isLoggable(Level.FINER)) {
2050                 stringBuilder.append(
2051                         format("\n Target Target Radius:           %9.3f Ã…\n", targetRadius));
2052                 stringBuilder.append(
2053                         format(" Asphericity:   (%6.3f Ã…) %9.3f\n", asphericity, asphericity / Rmax));
2054                 stringBuilder.append(format(" Acylindricity: (%6.3f Ã…) %9.3f\n", acylindricity,
2055                         acylindricity / Rmax));
2056                 stringBuilder.append(
2057                         format(" Anisotropy:    (%6.3f Ã…) %9.3f\n", sqrt(sqrt(anisotropy)),
2058                                 anisotropy / (Rg * Rg)));
2059                 stringBuilder.append(
2060                         format("  Ryz %9.3fÃ…  Rxz %9.3fÃ…  Rxy %9.3fÃ…\n", bestTargetRg[0],
2061                                 bestTargetRg[1], bestTargetRg[2]));
2062               }
2063             }
2064             if (logger.isLoggable(Level.FINER)) {
2065               stringBuilder.append(
2066                       format("\n Gyration Crystal 1 (%s): %7.4f Crystal 2 (%s): %7.4f.\n", name1,
2067                               gyrations[0], name2, gyrations[1]));
2068             }
2069             if (useSym) {
2070               StringBuilder sbSO = new StringBuilder(
2071                       format("\n Sym Op to move %s onto %s:\nsymop ", name2, name1));
2072               StringBuilder sbInv = new StringBuilder(
2073                       format("\n Inverted Sym Op to move %s onto %s:\nsymop ", name1, name2));
2074               final int[] mol1 = baseAssembly.getMoleculeNumbers();
2075               final int[] mol2 = targetAssembly.getMoleculeNumbers();
2076               for (int i = 0; i < z1; i++) {
2077                 for (int j = 0; j < z2; j++) {
2078                   if (bestTargetTransformSymOp[i][j] != null) {
2079                     bestTargetTransformSymOp[i][j] = bestTargetTransformSymOp[i][j].prepend(
2080                             bestTargetSymOp[i][j]);
2081                     // Apply inverse of base operations:
2082                     // For orthogonal matrices the inverse matrix = the transpose. True iff det(A)== +/- 1.
2083                     // No inverse if det(A)==0.
2084                     bestBaseTransformSymOp[i][j] = bestBaseTransformSymOp[i][j].prepend(
2085                             bestBaseSymOp[i][j]);
2086                     bestTargetTransformSymOp[i][j] = bestTargetTransformSymOp[i][j].append(
2087                             invertSymOp(bestBaseTransformSymOp[i][j]));
2088                     // Default to only printing out one sym Op per pair.
2089                     if (strict || logger.isLoggable(Level.FINEST) || z1 != z2 || i == j) {
2090                       SymOp inverted = invertSymOp(bestTargetTransformSymOp[i][j]);
2091                       if (logger.isLoggable(Level.FINEST)) {
2092                         stringBuilder.append(
2093                                 format("\n Sym Op to move %s (%2d) onto %s (%2d):\n", name2, j, name1,
2094                                         i)).append(bestTargetTransformSymOp[i][j]).append("\n");
2095                         stringBuilder.append(
2096                                 format("\n\n Inverted Sym Op to move %s (%2d) onto %s (%2d):\n", name1,
2097                                         i, name2, j)).append(inverted).append("\n");
2098                       }
2099                       ArrayList<Integer> mol1list = new ArrayList<>();
2100                       ArrayList<Integer> mol2list = new ArrayList<>();
2101                       for (int k = 0; k < nAtoms; k++) {
2102                         if ((z1 == 1 || i == mol1[k]) && ArrayUtils.contains(comparisonAtoms, k)) {
2103                           mol1list.add(k);
2104                         }
2105                         if ((z2 == 1 || j == mol2[k]) && ArrayUtils.contains(comparisonAtoms2, k)) {
2106                           mol2list.add(k);
2107                         }
2108                       }
2109                       final int[] mol1arr = mol1list.stream().mapToInt(Integer::intValue).toArray();
2110                       final int[] mol2arr = mol2list.stream().mapToInt(Integer::intValue).toArray();
2111                       sbSO.append(format("    %s     %s", writeAtomRanges(mol1arr), writeAtomRanges(mol2arr)))
2112                               .append(asMatrixString(bestTargetTransformSymOp[i][j]));
2113                       sbInv.append(format("    %s     %s", writeAtomRanges(mol2arr), writeAtomRanges(mol1arr))).append(asMatrixString(inverted));
2114                       if (i + 1 < z1 || j + 1 < z2) {
2115                         sbSO.append("\\\n");
2116                         sbInv.append("\\\n");
2117                       }
2118 
2119                       if (logger.isLoggable(Level.FINER)) {
2120                         stringBuilder.append(format(
2121                                 "\n Sym Op Application from Scratch: Target: %s (%2d) onto Base: %s (%2d)",
2122                                 name2, j, name1, i));
2123                         final double[] symMol = new double[nCoords];
2124                         for (int k = 0; k < compareAtomsSize; k++) {
2125                           final int l = k * 3;
2126                           final double[] xyz = new double[]{targetAUoriginal[j][l],
2127                                   targetAUoriginal[j][l + 1], targetAUoriginal[j][l + 2]};
2128                           applyCartesianSymOp(xyz, xyz, bestTargetTransformSymOp[i][j]);
2129                           symMol[l] = xyz[0];
2130                           symMol[l + 1] = xyz[1];
2131                           symMol[l + 2] = xyz[2];
2132                           stringBuilder.append(format(
2133                                   "\n %8.3f %8.3f %8.3f moved to %8.3f %8.3f %8.3f compared to %8.3f %8.3f %8.3f",
2134                                   targetAUoriginal[j][l], targetAUoriginal[j][l + 1],
2135                                   targetAUoriginal[j][l + 2], symMol[l], symMol[l + 1], symMol[l + 2],
2136                                   baseAUoriginal[i][l], baseAUoriginal[i][l + 1],
2137                                   baseAUoriginal[i][l + 2]));
2138                         }
2139                         if (saveClusters > 0) {
2140                           saveAssembly(file2, name2, bondList2, atoms2, forceField2, symMol,
2141                                   comparisonAtoms, 1, "_moved", 0, saveClusters);
2142                         }
2143 
2144                         final double value = rmsd(baseAUoriginal[i], symMol, massN);
2145                         final double[] symMol2 = new double[nCoords];
2146                         stringBuilder.append("\n\n Sym Op Inverse Application:");
2147                         for (int k = 0; k < compareAtomsSize; k++) {
2148                           final int l = k * 3;
2149                           final double[] xyz = new double[]{symMol[l], symMol[l + 1], symMol[l + 2]};
2150                           applyCartesianSymOp(xyz, xyz, inverted);
2151                           symMol2[l] = xyz[0];
2152                           symMol2[l + 1] = xyz[1];
2153                           symMol2[l + 2] = xyz[2];
2154                           stringBuilder.append(format(
2155                                   "\n %8.3f %8.3f %8.3f moved to %8.3f %8.3f %8.3f compared to %8.3f %8.3f %8.3f",
2156                                   symMol[l], symMol[l + 1], symMol[l + 2], symMol2[l], symMol2[l + 1],
2157                                   symMol2[l + 2], targetAUoriginal[j][l], targetAUoriginal[j][l + 1],
2158                                   targetAUoriginal[j][l + 2]));
2159                         }
2160                         stringBuilder.append(format("\n\n Sym Op RMSD: %8.8f \n\n", value));
2161                       }
2162                     }
2163                   } else {
2164                     if (logger.isLoggable(Level.INFO)) {
2165                       logger.info(format(" Symmetry Operator %2d %2d was filtered out. "
2166                               + "Try using a single asymmetric unit (--na 1).", i, j));
2167                     }
2168                   }
2169                 }
2170               }
2171               stringBuilder.append(sbSO.toString()).append("\n").append(sbInv.toString())
2172                       .append("\n");
2173             }
2174             // Reset values for base system if they were swapped.
2175             if (!densityCheck && crystalPriority != 2) {
2176               baseXYZ = new double[nTargetCoords];
2177               arraycopy(targetXYZ, 0, baseXYZ, 0, nTargetCoords);
2178               baseXYZoriginal = new double[nTargetCoords];
2179               arraycopy(targetXYZoriginal, 0, baseXYZoriginal, 0, nTargetCoords);
2180               arraycopy(reducedTargetCoords, 0, reducedBaseCoords, 0, nCoords);
2181               baseSymOps = new ArrayList<>(targetSymOps);
2182               baseDistMap = new ArrayList<>(targetDistMap);
2183               int[] tempAtoms = comparisonAtoms.clone();
2184               comparisonAtoms = comparisonAtoms2.clone();
2185               comparisonAtoms2 = tempAtoms.clone();
2186               DoubleIndexPair[] tempDIP = new DoubleIndexPair[nBaseMols];
2187               arraycopy(baseAUDist, 0, tempDIP, 0, nBaseMols);
2188               baseAUDist = new DoubleIndexPair[nTargetMols];
2189               arraycopy(targetAUDist, 0, baseAUDist, 0, nTargetMols);
2190               targetAUDist = new DoubleIndexPair[nBaseMols];
2191               arraycopy(tempDIP, 0, targetAUDist, 0, nBaseMols);
2192               final double[][] tempDD = new double[nBaseMols][3];
2193               arraycopy(baseCoM[0], 0, tempDD, 0, nBaseMols);
2194               baseCoM = new double[3][nTargetMols][3];
2195               arraycopy(targetCoM, 0, baseCoM[0], 0, nTargetMols);
2196               targetCoM = new double[nBaseMols][3];
2197               arraycopy(tempDD, 0, targetCoM, 0, nBaseMols);
2198               int temp = z1;
2199               z1 = z2;
2200               z2 = temp;
2201               temp = baseSearchValue;
2202               baseSearchValue = targetSearchValue;
2203               targetSearchValue = temp;
2204               nBaseCoords = nTargetCoords;
2205               nBaseMols = nTargetMols;
2206             }
2207             if (hitTol >= 0.0 && hitTol > rmsd) {
2208               numberOfHits++;
2209             }
2210           }
2211           myDistances[myIndex] = rmsd;
2212           myIndex++;
2213           if (!stringBuilder.isEmpty()) {
2214             logger.info(stringBuilder.toString());
2215           }
2216           if (save >= 0.0 && rmsd < save) {
2217             XYZFilter xyzFilter = new XYZFilter(saveLocation1.getAbsoluteFile(), baseAssembly, baseAssembly.getForceField(),
2218                     baseAssembly.getProperties());
2219             xyzFilter.writeFile(saveLocation1.getAbsoluteFile(), true);
2220             XYZFilter xyzFilter2 = new XYZFilter(saveLocation2.getAbsoluteFile(), targetAssembly, targetAssembly.getForceField(),
2221                     targetAssembly.getProperties());
2222             xyzFilter2.writeFile(saveLocation2.getAbsoluteFile(), true);
2223           }
2224         }
2225         if (lowMemory) {
2226           targetFilter.readNext(false, false, (column + 1) % numProc == rank);
2227         }
2228       }
2229       restartColumn = 0;
2230       if (lowMemory) {
2231         targetFilter.readNext(true, false, 0 == rank);
2232       }
2233       baseFilter.readNext(false, false);
2234 
2235       // Gather RMSDs for this row.
2236       gatherRMSDs(row, runningStatistics);
2237 
2238       // Write out this row.
2239       if (rank == 0 && write) {
2240         final int firstColumn = (isSymmetric) ? row : 0;
2241         writeDistanceMatrixRow(pacFileName, distRow, firstColumn);
2242       }
2243     }
2244 
2245     if (minTime < Double.MAX_VALUE) {
2246       logger.info(format("\n Minimum PAC Time: %7.4f", minTime));
2247     }
2248     if (logger.isLoggable(Level.FINE) && maxTime > Double.MIN_VALUE && maxTime != minTime) {
2249       logger.info(format(" Maximum PAC Time: %7.4f", maxTime));
2250     }
2251 
2252     baseFilter.closeReader();
2253     targetFilter.closeReader();
2254 
2255     if (rank == 0 || logger.isLoggable(Level.FINER)) {
2256       final double min = runningStatistics.getMin();
2257       if (Double.MAX_VALUE - min < matchTol) {
2258         logger.warning(" SuperposeCrystals was not able to compare the provided structures.");
2259         if (logger.isLoggable(Level.FINE)) {
2260           logger.info(format(" RMSD Minimum:  %8.6f", min));
2261         }
2262       } else {
2263         if(hitTol >= 0.0){
2264           logger.info(format(" Number of \"Hits\":  %8d", numberOfHits));
2265         }
2266         logger.info(format(" RMSD Minimum:  %8.6f", min));
2267         logger.info(format(" RMSD Maximum:  %8.6f", runningStatistics.getMax()));
2268         logger.info(format(" RMSD Mean:     %8.6f", runningStatistics.getMean()));
2269         final double variance = runningStatistics.getVariance();
2270         if (!Double.isNaN(variance)) {
2271           logger.info(format(" RMSD Variance: %8.6f", variance));
2272         }
2273       }
2274     }
2275 
2276     // Return distMatrix for validation if this is for the test script
2277     return runningStatistics;
2278   }
2279 
2280   /**
2281    * Add a value to a list of doubles if its difference to all listed values is greater than the
2282    * tolerance.
2283    *
2284    * @param values List of values already found.
2285    * @param value  Potential new value if it is not already in list.
2286    * @param tol    Tolerance that determine whether values are equal.
2287    */
2288   private static boolean addLooseUnequal(List<Double> values, final double value, final double tol) {
2289     boolean found = false;
2290     for (Double dbl : values) {
2291       if (abs(dbl - value) < tol) {
2292         found = true;
2293         break;
2294       }
2295     }
2296     if (!found) {
2297       values.add(value);
2298     }
2299     // If value is not found it was added. Otherwise, not added.
2300     return !found;
2301   }
2302 
2303   /**
2304    * Accumulate rotations (matrix multiplication)
2305    *
2306    * @param rot            Rotation matrix to add.
2307    * @param totalTransform Array to be updated with rotation (4x4).
2308    * @param prepend        If true prepend translation, false append to end.
2309    */
2310   public void addRotation(final double[][] rot, final double[][] totalTransform, final boolean prepend) {
2311     double[][] transform = new double[][]{{rot[0][0], rot[0][1], rot[0][2], 0.0},
2312         {rot[1][0], rot[1][1], rot[1][2], 0.0}, {rot[2][0], rot[2][1], rot[2][2], 0.0},
2313         {0.0, 0.0, 0.0, 1.0}};
2314     transform =
2315         (prepend) ? mat4Mat4(totalTransform, transform) : mat4Mat4(transform, totalTransform);
2316     final int length = totalTransform.length;
2317     for (int i = 0; i < length; i++) {
2318       arraycopy(transform[i], 0, totalTransform[i], 0, totalTransform[i].length);
2319     }
2320   }
2321 
2322   /**
2323    * Accumulate translations (matrix multiplication)
2324    *
2325    * @param translation    Translation matrix to add.
2326    * @param totalTransform Array to be updated with translation (4x4).
2327    * @param prepend        If true prepend translation, false append to end.
2328    */
2329   public static void addTranslation(final double[] translation, final double[][] totalTransform,
2330                                     final boolean prepend) {
2331     double[][] transform = new double[][]{{1.0, 0.0, 0.0, translation[0]},
2332         {0.0, 1.0, 0.0, translation[1]}, {0.0, 0.0, 1.0, translation[2]}, {0.0, 0.0, 0.0, 1.0}};
2333     transform =
2334         (prepend) ? mat4Mat4(totalTransform, transform) : mat4Mat4(transform, totalTransform);
2335     final int length = totalTransform.length;
2336     for (int i = 0; i < length; i++) {
2337       arraycopy(transform[i], 0, totalTransform[i], 0, totalTransform[i].length);
2338     }
2339   }
2340 
2341   /**
2342    * Calculate the center of mass for a given set of masses for the asymmetric unit and coordinates
2343    * (xyz)
2344    *
2345    * @param centersOfMass Returned center of mass for each asymmetric unit
2346    * @param coords        Coordinates of every atom in system.
2347    * @param mass          Masses of each atom in asymmetric unit.
2348    * @param massSum       Sum of masses within asymmetric unit.
2349    * @param nAtoms        Number of atoms in an entity.
2350    */
2351   private static void centerOfMass(final double[][] centersOfMass, final double[] coords, final double[] mass,
2352                                    final double massSum, final int nAtoms) {
2353     final int nAU = coords.length / (nAtoms * 3);
2354     for (int i = 0; i < nAU; i++) {
2355       final int auIndex = i * nAtoms * 3;
2356       final double[] comI = new double[3];
2357       for (int j = 0; j < nAtoms; j++) {
2358         final int atomIndex = auIndex + j * 3;
2359         final double m = mass[j];
2360         for (int k = 0; k < 3; k++) {
2361           comI[k] += coords[atomIndex + k] * m;
2362         }
2363       }
2364       for (int j = 0; j < 3; j++) {
2365         comI[j] /= massSum;
2366       }
2367       centersOfMass[i] = comI;
2368     }
2369   }
2370 
2371   /**
2372    * Determine if replicates crystal is large enough for approximate cluster to be used in PAC
2373    * comparison.
2374    *
2375    * @param xyz              XYZ coordinates of structures.
2376    * @param massN            Masses per atom.
2377    * @param massSum          Sum of masses.
2378    * @param compareAtomsSize Number of atoms being compared from each structure.
2379    * @param nAU              Number of structures to compare between crystals.
2380    * @param linkage          Linkage method to be used.
2381    * @param startLMN         Current values for L x M x N (overwritten with new values)
2382    * @return Whether the LMN values have changed. If yes, recalculate replicates.
2383    */
2384   private static boolean checkInflatedSphere(final double[] xyz, final double[] massN, final double massSum,
2385                                              final int compareAtomsSize, final int nAU, final int linkage, final int[] startLMN, final ArrayList<SymOp> symOps,
2386                                              final ArrayList<Integer> indexOrder) {
2387     // Recalculate center of mass based on XYZ order ([0] is the closest to center).
2388     final double[][] centerOfMass = new double[xyz.length / 3][3];
2389     centerOfMass(centerOfMass, xyz, massN, massSum, compareAtomsSize);
2390     DoubleIndexPair[] auDist = new DoubleIndexPair[xyz.length / 3 / compareAtomsSize];
2391     prioritizeReplicates(xyz, centerOfMass, compareAtomsSize, auDist, 0, linkage);
2392 
2393     boolean redo = false;
2394     for (int i = 0; i < nAU; i++) {
2395       if (logger.isLoggable(Level.FINE)) {
2396         logger.fine(format(
2397             " i: %3d\t auDist Index: %3d \t indexOrder: %3d\t indget(au): %3d\t ReplicatesVector:  %2d (%2d) %2d (%2d) %2d (%2d)",
2398             i, auDist[i].index(), indexOrder.get(i), indexOrder.get(auDist[i].index()),
2399             symOps.get(indexOrder.get(auDist[i].index())).replicatesVector[0] + 1, startLMN[0],
2400             symOps.get(indexOrder.get(auDist[i].index())).replicatesVector[1] + 1, startLMN[1],
2401             symOps.get(indexOrder.get(auDist[i].index())).replicatesVector[2] + 1, startLMN[2]));
2402       }
2403       final int[] lmn = symOps.get(indexOrder.get(auDist[i].index())).replicatesVector;
2404       if (lmn[0] == 0 || lmn[0] == startLMN[0] - 1) {
2405         redo = true;
2406       }
2407       if (lmn[1] == 0 || lmn[1] == startLMN[1] - 1) {
2408         redo = true;
2409       }
2410       if (lmn[2] == 0 || lmn[2] == startLMN[2] - 1) {
2411         redo = true;
2412       }
2413       if (redo) {
2414         break;
2415       }
2416     }
2417     if (redo && logger.isLoggable(Level.FINE)) {
2418       logger.fine(" REDO EXPANSION.");
2419     }
2420     return redo;
2421   }
2422 
2423   /**
2424    * Deep copy of values from one array to another.
2425    *
2426    * @param newArray Destination of values.
2427    * @param oldArray Current location of values.
2428    */
2429   private static void copyArrayValues(double[][] newArray, double[][] oldArray) {
2430     final int arrayLength = newArray.length;
2431     for (int i = 0; i < arrayLength; i++) {
2432       arraycopy(oldArray[i], 0, newArray[i], 0, newArray[i].length);
2433     }
2434   }
2435 
2436   /**
2437    * Determine the indices of the atoms from the assembly that are active for this comparison.
2438    *
2439    * @param atoms           Atoms we potentially wish to use in comparison.
2440    * @param indices         Array list containing atom indices that will be used for this comparison.
2441    * @param unique          Indices that are unique to the system and should not be included in
2442    *                        comparison.
2443    * @param alphaCarbons    Boolean whether to include only alpha carbon/nitrogen atoms.
2444    * @param includeHydrogen Boolean whether to include hydrogen atoms.
2445    */
2446   private static void determineComparableAtoms(Atom[] atoms, ArrayList<Integer> indices,
2447                                                ArrayList<Integer> unique, boolean alphaCarbons, boolean includeHydrogen) {
2448     final int nAtoms = atoms.length;
2449     for (int i = 0; i < nAtoms; i++) {
2450       if (!unique.contains(i)) {
2451         final Atom atom = atoms[i];
2452         if (alphaCarbons) {
2453           final String atomName = atom.getName();
2454           final int atomAtNum = atom.getAtomicNumber();
2455           final boolean proteinCheck = atomName.equals("CA") && atomAtNum == 6;
2456           final boolean aminoCheck = (atomName.equals("N1") || atomName.equals("N9")) && atomAtNum == 7;
2457           if (proteinCheck || aminoCheck) {
2458             indices.add(i);
2459           }
2460         } else if (includeHydrogen || !atom.isHydrogen()) {
2461           indices.add(i);
2462         }
2463         // Reset all atoms to active once the selection is recorded.
2464         atom.setActive(true);
2465       }
2466     }
2467   }
2468 
2469   /**
2470    * This method calls <code>world.gather</code> to collect numProc PAC RMSD values.
2471    *
2472    * @param row               Current row of the PAC RMSD matrix.
2473    * @param runningStatistics Stats for the RMSDs.
2474    */
2475   private void gatherRMSDs(int row, RunningStatistics runningStatistics) {
2476     if (useMPI) {
2477       try {
2478         if (logger.isLoggable(Level.FINER)) {
2479           logger.finer(" Receiving MPI results.");
2480         }
2481         // TODO: Node 0 is the only process that updates. Remove from other nodes.
2482         world.gather(0, myBuffer, buffers);
2483         if (rank == 0) {
2484           for (int workItem = 0; workItem < numWorkItems; workItem++) {
2485             for (int proc = 0; proc < numProc; proc++) {
2486               final int column = numProc * workItem + proc;
2487               // Do not include padded results.
2488               if (column < targetSize) {
2489                 distRow[column] = distances[proc][workItem];
2490                 if (!isSymmetric) {
2491                   runningStatistics.addValue(distRow[column]);
2492                 } else if (column >= row) {
2493                   // Only collect stats for the upper triangle.
2494                   runningStatistics.addValue(distRow[column]);
2495                 }
2496                 if (logger.isLoggable(Level.FINER)) {
2497                   logger.finer(format(" %d %d %14.8f", row, column, distances[proc][workItem]));
2498                 }
2499               }
2500             }
2501           }
2502         } else {
2503           for (int workItem = 0; workItem < numWorkItems; workItem++) {
2504             final int column = numProc * workItem + rank;
2505             // Do not include padded results.
2506             if (column < targetSize) {
2507               distRow[column] = distances[rank][workItem];
2508               if (!isSymmetric) {
2509                 runningStatistics.addValue(distRow[column]);
2510               } else if (column >= row) {
2511                 // Only collect stats for the upper triangle.
2512                 runningStatistics.addValue(distRow[column]);
2513               }
2514               if (logger.isLoggable(Level.FINER)) {
2515                 logger.finer(format(" %d %d %14.8f", row, column, distances[rank][workItem]));
2516               }
2517             }
2518           }
2519         }
2520       } catch (Exception e) {
2521         logger.severe(" Exception collecting distance values." + e);
2522       }
2523     } else {
2524       for (int i = 0; i < targetSize; i++) {
2525         distRow[i] = myDistances[i];
2526         if (!isSymmetric) {
2527           runningStatistics.addValue(distRow[i]);
2528         } else if (i >= row) {
2529           // Only collect stats for the upper triangle.
2530           runningStatistics.addValue(distRow[i]);
2531         }
2532       }
2533     }
2534   }
2535 
2536   /**
2537    * Generate and expanded sphere of asymmetric unit with the intention of observing a crystals'
2538    * distribution of replicates to facilitate comparisons that go beyond lattice parameters.
2539    *
2540    * @param unitCell        Crystal to define expansion.
2541    * @param reducedCoords   Coordinates of asymmetric unit we wish to expand.
2542    * @param zPrime          Number of molecules in asymmetric unit.
2543    * @param nAU             Number of asymmetric units to compare in final shell (used to determine expansion
2544    *                        guess).
2545    * @param linkage         Type of linkage to be used in comparison (single, average, or complete).
2546    * @param mass            Masses for atoms within reduced asymmetric unit.
2547    * @param lmn             Replicates lattice vector lengths (L x M x N).
2548    * @param symOps          List of symmetry operators used to expand to replicates crystal.
2549    * @param indexOrder      Sorted list of index values (e.g. index 0 is contained in input file but
2550    *                        may not be first).
2551    * @param inflationFactor Scalar to over expand replicates crystal to obtain all necessary
2552    *                        orientations.
2553    * @return double[] containing the coordinates for the expanded crystal.
2554    */
2555   private static double[] generateInflatedSphere(final Crystal unitCell,
2556                                                  final double[] reducedCoords, final int zPrime, final int nAU, final int linkage,
2557                                                  final double[] mass, int[] lmn, final boolean strict, ArrayList<SymOp> symOps,
2558                                                  ArrayList<Integer> indexOrder, double inflationFactor) {
2559     symOps.clear();
2560     indexOrder.clear();
2561     // Num coords in asymmetric unit
2562     final int nCoords = reducedCoords.length;
2563     // Num atoms in asymmetric unit
2564     final int nAtoms = nCoords / 3;
2565     // Num atoms per molecule
2566     final int zAtoms = nAtoms / zPrime;
2567     // Collect asymmetric unit atomic coordinates.
2568     final double[] x = new double[nAtoms];
2569     final double[] y = new double[nAtoms];
2570     final double[] z = new double[nAtoms];
2571     for (int i = 0; i < nAtoms; i++) {
2572       final int atomIndex = i * 3;
2573       x[i] = reducedCoords[atomIndex];
2574       y[i] = reducedCoords[atomIndex + 1];
2575       z[i] = reducedCoords[atomIndex + 2];
2576     }
2577 
2578     final double volume = unitCell.volume / unitCell.getNumSymOps() / zPrime;
2579     final double radius = cbrt(0.75 / PI * volume * max(3, nAU) * inflationFactor);
2580     Crystal replicatesCrystal = ReplicatesCrystal.replicatesCrystalFactory(unitCell, radius * 2.0,
2581         lmn);
2582     // Used for LMN specific replicates expansion.
2583 //    Crystal replicatesCrystal = new ReplicatesCrystal(unitCell, lmn[0], lmn[1], lmn[2], radius * 2.0);
2584 //    startLMN = lmn;
2585     // Symmetry coordinates for each molecule in replicates crystal
2586     final int nSymm = replicatesCrystal.getNumSymOps();
2587     final double[][] xS = new double[nSymm][nAtoms];
2588     final double[][] yS = new double[nSymm][nAtoms];
2589     final double[][] zS = new double[nSymm][nAtoms];
2590 
2591     final int numEntities = nSymm * zPrime;
2592     // Cartesian center of each molecule
2593     final double[][] cartCenterOfMass = new double[numEntities][3];
2594 
2595     // Loop over replicate crystal SymOps
2596     List<SymOp> inflatedSymOps = replicatesCrystal.spaceGroup.symOps;
2597     for (int iSym = 0; iSym < nSymm; iSym++) {
2598       final SymOp symOp = inflatedSymOps.get(iSym);
2599       //Convert sym op into cartesian for later use.
2600       final double[][] rot = new double[3][3];
2601       replicatesCrystal.getTransformationOperator(symOp, rot);
2602       // Apply SymOp to the asymmetric unit atoms Cartesian Coordinates.
2603       replicatesCrystal.applySymOp(nAtoms, x, y, z, xS[iSym], yS[iSym], zS[iSym], symOp);
2604       for (int zp = 0; zp < zPrime; zp++) {
2605         final int symIndex = zp * zAtoms;
2606         // Compute center-of-mass (CoM) for Cartesian coordinates
2607         final double[] centerOfMass = new double[3];
2608         double zMass = 0.0;
2609         for (int i = 0; i < zAtoms; i++) {
2610           final double m = mass[i];
2611           final int coordIndex = symIndex + i;
2612           centerOfMass[0] += xS[iSym][coordIndex] * m;
2613           centerOfMass[1] += yS[iSym][coordIndex] * m;
2614           centerOfMass[2] += zS[iSym][coordIndex] * m;
2615           zMass += m;
2616         }
2617         centerOfMass[0] /= zMass;
2618         centerOfMass[1] /= zMass;
2619         centerOfMass[2] /= zMass;
2620 
2621         final double[] translate = moveIntoCrystal(replicatesCrystal, centerOfMass);
2622         //Convert sym op into cartesian for later use.
2623         final double[] trans = symOp.tr.clone();
2624         replicatesCrystal.toCartesianCoordinates(trans, trans);
2625         final int translateLength = translate.length;
2626         for (int i = 0; i < translateLength; i++) {
2627           trans[i] += translate[i];
2628         }
2629         symOps.add(new SymOp(rot, trans, symOp.replicatesVector));
2630         for (int i = 0; i < zAtoms; i++) {
2631           final int coordIndex = symIndex + i;
2632           xS[iSym][coordIndex] += translate[0];
2633           centerOfMass[0] += translate[0];
2634           yS[iSym][coordIndex] += translate[1];
2635           centerOfMass[1] += translate[1];
2636           zS[iSym][coordIndex] += translate[2];
2637           centerOfMass[2] += translate[2];
2638         }
2639 
2640         // Save CoM cartesian coordinates
2641         cartCenterOfMass[iSym * zPrime + zp] = centerOfMass;
2642       }
2643     }
2644 
2645     //Determine molecular distances to "center" of sphere.
2646     //  In PACCOM the center is the geometric average of coordinates.
2647     //  In FFX the center is the center of the replicates crystal.
2648     final DoubleIndexPair[] auDist = new DoubleIndexPair[numEntities];
2649     final double[] cartCenter = new double[3];
2650 
2651     // Save (mark) a molecule as being closest to the center of the replicates crystal (0.5, 0.5, 0.5)
2652     // Convert (0.5, 0.5, 0.5) to Cartesian Coordinates
2653     final double[] fracCenter = {0.5, 0.5, 0.5};
2654     replicatesCrystal.toCartesianCoordinates(fracCenter, cartCenter);
2655 
2656     if (logger.isLoggable(Level.FINER)) {
2657       if (logger.isLoggable(Level.FINEST)) {
2658         logger.finer(" Replicates crystal " + replicatesCrystal);
2659       }
2660       logger.finer(format(" Replicates Volume: %8.4f", replicatesCrystal.volume));
2661       logger.finer(
2662           format(" Expanded Crystal Center: %14.8f %14.8f %14.8f", cartCenter[0], cartCenter[1],
2663               cartCenter[2]));
2664     }
2665     logger.fine(
2666         format(" Expanded Crystal Center: %14.8f %14.8f %14.8f", cartCenter[0], cartCenter[1],
2667             cartCenter[2]));
2668 
2669     for (int i = 0; i < numEntities; i++) {
2670       // Then compute Euclidean distance from Cartesian center of the replicates cell
2671       auDist[i] = new DoubleIndexPair(i, dist2(cartCenter, cartCenterOfMass[i]));
2672     }
2673 
2674     // Sort the molecules by their distance from the center.
2675     // Note that the smallest distances are first in the array after the sort.
2676     sort(auDist);
2677     for (DoubleIndexPair molsDist : auDist) {
2678       indexOrder.add(molsDist.index());
2679     }
2680 
2681     if (logger.isLoggable(Level.FINEST)) {
2682       logger.finest("\n Copy  SymOp        Distance");
2683     }
2684     double[] systemCoords = new double[numEntities * zAtoms * 3];
2685     for (int n = 0; n < nSymm; n++) {
2686       for (int zp = 0; zp < zPrime; zp++) {
2687         final int index = n * zPrime + zp;
2688         // Current molecule
2689         final int iSym = auDist[index].index();
2690         final double distance = auDist[index].doubleValue();
2691         if (logger.isLoggable(Level.FINEST) && n < 30) {
2692           logger.finest(format(" %4d  %5d  %14.8f", index, iSym, sqrt(distance)));
2693         }
2694 
2695         // Create a new set of Atoms for each SymOp molecule
2696         for (int i = 0; i < zAtoms; i++) {
2697           final int symIndex = index * zAtoms * 3;
2698           final int atomIndex = symIndex + i * 3;
2699           final int conversion = iSym / zPrime;
2700           final int shift = i + (iSym % zPrime) * zAtoms;
2701           systemCoords[atomIndex] = xS[conversion][shift];
2702           systemCoords[atomIndex + 1] = yS[conversion][shift];
2703           systemCoords[atomIndex + 2] = zS[conversion][shift];
2704         }
2705       }
2706     }
2707     final double massSum = Arrays.stream(mass).sum();
2708     if (logger.isLoggable(Level.FINE)) {
2709       logger.fine(" Checking replicates crystal.");
2710       logger.fine(format(
2711           " SysCoords: %3d (%3d)(%3d), reducedCoords Size: %3d (%3d)\n mass Size %3d, massSum: %6.3f, nAU: %2d, nCoords: %3d\n "
2712               + "auDist Size: %3d, l: %2d, m: %2d, n: %2d, numEntities: %3d, nAtoms: %3d, zPrime: %3d, zAtoms: %3d",
2713           systemCoords.length, systemCoords.length / 3, systemCoords.length / 3 / zAtoms,
2714           reducedCoords.length, reducedCoords.length / 3, mass.length, massSum, nAU, nCoords,
2715           auDist.length, lmn[0], lmn[1], lmn[2], numEntities, nAtoms, zPrime, zAtoms));
2716     }
2717     if (strict && checkInflatedSphere(systemCoords, mass, massSum, zAtoms, nAU, linkage, lmn, symOps,
2718         indexOrder)) {
2719       // Used for LMN specific replicates expansion.
2720 //        startLMN = determineExpansion(unitCell, reducedCoords, comparisonAtoms, mass, nAU, zPrime, inflationFactor);
2721       inflationFactor += 5;
2722       systemCoords = generateInflatedSphere(unitCell, reducedCoords, zPrime, nAU, linkage, mass, lmn,
2723           true, symOps, indexOrder, inflationFactor);
2724     }
2725     return systemCoords;
2726   }
2727 
2728   /**
2729    * Try to automatically determine number of species in asymmetric unit (only works for molecules).
2730    *
2731    * @param unique      List of atom indices to use in comparison.
2732    * @param numEntities Number of species detected.
2733    * @return Number of expected species in asymmetric unit.
2734    */
2735   private static int guessZPrime(final ArrayList<Integer> unique, final int[] molNum, final int numEntities) {
2736     final int[] molSize = new int[numEntities];
2737     final int nAtoms = molNum.length;
2738     for(int i = 0; i < nAtoms; i++){
2739       if(unique.contains(i)){
2740         molSize[molNum[i]]++;
2741       }
2742     }
2743     int z;
2744     switch (numEntities) {
2745       case 2 -> {
2746         if (molSize[0] == molSize[1]) {
2747           z = 2;
2748         } else {
2749           z = 1;
2750         }
2751       }
2752       case 3 -> {
2753         if (molSize[0] == molSize[1] && molSize[1] == molSize[2]) {
2754           z = 3;
2755         } else {
2756           z = 1;
2757         }
2758       }
2759       case 4 -> {
2760         if (molSize[0] == molSize[1] && molSize[1] == molSize[2] && molSize[2] == molSize[3]) {
2761           z = 4;
2762         } else if (molSize[0] == molSize[2] && molSize[1] == molSize[3]) {
2763           z = 2;
2764         } else {
2765           z = 1;
2766         }
2767       }
2768       case 5 -> {
2769         if (molSize[0] == molSize[1] && molSize[1] == molSize[2] && molSize[2] == molSize[3] && molSize[3] == molSize[4]) {
2770           z = 5;
2771         } else {
2772           z = 1;
2773         }
2774       }
2775       case 6 -> {
2776         if (molSize[0] == molSize[1] && molSize[1] == molSize[2] && molSize[2] == molSize[3] && molSize[3] == molSize[4] && molSize[4] == molSize[5]) {
2777           z = 6;
2778         } else if (molSize[0] == molSize[2] && molSize[2] == molSize[4] && molSize[1] == molSize[3] && molSize[3] == molSize[5]) {
2779           z = 3;
2780         } else if (molSize[0] == molSize[3] && molSize[1] == molSize[4] && molSize[2] == molSize[5]) {
2781           z = 2;
2782         } else {
2783           z = 1;
2784         }
2785       }
2786       default -> z = 1;
2787     }
2788     if (logger.isLoggable(Level.FINER)) {
2789       logger.finer(format(" Number of species in asymmetric unit (Z Prime): %d", z));
2790     }
2791     return z;
2792   }
2793 
2794   /**
2795    * Determine if the user selected atoms are invalid.
2796    *
2797    * @param indices         Atom indices to be included in this comparison.
2798    * @param atoms           All available atoms.
2799    * @param alphaCarbons    Only include alpha carbons.
2800    * @param includeHydrogen Include hydrogen atoms.
2801    * @return Whether any atoms were selected.
2802    */
2803   private static boolean invalidAtomSelection(final ArrayList<Integer> indices, final Atom[] atoms,
2804                                               final boolean alphaCarbons, final boolean includeHydrogen) {
2805     final ArrayList<Integer> unique = new ArrayList<>();
2806     for (Integer value : indices) {
2807       if (!unique.contains(value)) {
2808         unique.add(value);
2809       }
2810     }
2811     indices.clear();
2812     determineComparableAtoms(atoms, indices, unique, alphaCarbons, includeHydrogen);
2813     return indices.isEmpty();
2814   }
2815 
2816   /**
2817    * Parse values of a matrix into a string.
2818    *
2819    * @param matrix      Values to present
2820    * @param index       Identifier
2821    * @param description Identifier
2822    * @return String of values.
2823    */
2824   public static String matrixToString(final double[][] matrix, final int index, String description) {
2825     StringBuilder value = new StringBuilder(format("\n %s %d: ", description, index));
2826     for (double[] doubles : matrix) {
2827       value.append(format("\n\t%s", Arrays.toString(doubles)));
2828     }
2829     value.append("\n");
2830     return value.toString();
2831   }
2832 
2833   /**
2834    * Produce a translation vector necessary to move an object with the current center of mass (com)
2835    * into the provided crystal.
2836    *
2837    * @param crystal Replicates crystal within whom coordinates should be moved.
2838    * @param com     Center of mass (x, y, z) for the object of concern
2839    * @return double[] translation vector to move the object within the provided crystal.
2840    */
2841   private static double[] moveIntoCrystal(final Crystal crystal, final double[] com) {
2842 
2843     final double[] translate = new double[3];
2844     final double[] currentCoM = new double[3];
2845     currentCoM[0] = com[0];
2846     currentCoM[1] = com[1];
2847     currentCoM[2] = com[2];
2848 
2849     // Move the COM to the Replicates Crystal.
2850     crystal.toFractionalCoordinates(com, translate);
2851     translate[0] = mod(translate[0], 1.0);
2852     translate[1] = mod(translate[1], 1.0);
2853     translate[2] = mod(translate[2], 1.0);
2854     crystal.toCartesianCoordinates(translate, translate);
2855 
2856     // Correct center of mass.
2857     com[0] = translate[0];
2858     com[1] = translate[1];
2859     com[2] = translate[2];
2860     // The translation vector is difference between the new location and the current COM.
2861     translate[0] -= currentCoM[0];
2862     translate[1] -= currentCoM[1];
2863     translate[2] -= currentCoM[2];
2864 
2865     return translate;
2866   }
2867 
2868   /**
2869    * Determine the number of unique AUs within the replicates crystal to a tolerance.
2870    *
2871    * @param allCoords       Coordinates for every atom in replicates crystal ([x1, y1, z1, x2, y2,
2872    *                        z2...].
2873    * @param auDist          Prioritization of molecules.
2874    * @param auCoords        Coordinates for single AU.
2875    * @param nCoords         Number of coordinates in an AU (number of atoms * 3).
2876    * @param upperLimit      The largest number of unique AUs (0 for no upper limit).
2877    * @param strict          Search entire replicates crystal if true, otherwise only the expected.
2878    * @param nAUinReplicates Number of AUs in replicates crystal.
2879    * @param massN           Array containing masses for each atom in AU.
2880    * @param matchTol        Tolerance to determine whether two AUs are the same.
2881    */
2882   private void numberUniqueAUs(double[] allCoords, DoubleIndexPair[] auDist, double[] auCoords,
2883                                int nCoords, int upperLimit, boolean strict, int nAUinReplicates, double[] massN,
2884                                double matchTol) {
2885     // uniqueDiffs is only recorded for logging... could remove later.
2886     // List of differences (RMSD_1) for AUs in replicates crystal.
2887     final List<Double> uniqueDiffs = new ArrayList<>();
2888     final ArrayList<double[]> tempListXYZ = new ArrayList<>();
2889     tempListIndices.add(0);
2890     uniqueDiffs.add(rmsd(auCoords, auCoords, massN));
2891     tempListXYZ.add(auCoords);
2892     // Start from 1 as zero is automatically added.
2893     int index = 1;
2894     // Determine number of conformations in first crystal
2895     final boolean useSym = printSym >= 0.0;
2896     final int numConfCheck = (strict || upperLimit <= 0 || useSym) ? nAUinReplicates : upperLimit;
2897     if (logger.isLoggable(Level.FINEST)) {
2898       logger.finest("RMSD Differences in Replicates Crystal:");
2899     }
2900     final double[] target = new double[nCoords];
2901     arraycopy(tempListXYZ.get(0), 0, target, 0, nCoords);
2902     translate(target, massN);
2903     while ((uniqueDiffs.size() < numConfCheck)) {
2904       final double[] baseCheckMol = new double[nCoords];
2905       final int auIndex = auDist[index].index();
2906       arraycopy(allCoords, auIndex * nCoords, baseCheckMol, 0, nCoords);
2907       translate(baseCheckMol, massN);
2908       rotate(target, baseCheckMol, massN);
2909       final double value = rmsd(target, baseCheckMol, massN);
2910       if (addLooseUnequal(uniqueDiffs, value, matchTol) || useSym) {
2911         if (logger.isLoggable(Level.FINEST)) {
2912           logger.finest(format(" Sorted Index: %4d  Dist: %18.16f", auIndex, value));
2913         }
2914         tempListIndices.add(index);
2915         tempListXYZ.add(baseCheckMol);
2916       }
2917       index++;
2918       if (index >= auDist.length) {
2919         break;
2920       }
2921     }
2922     if (logger.isLoggable(Level.FINER)) {
2923       logger.finer(" RMSD_1 from 1st AU:\n i RMSD     AU Index");
2924       final int numUnique = uniqueDiffs.size();
2925       for (int i = 0; i < numUnique; i++) {
2926         logger.finer(format(" %d %4.4f    %4d", i, uniqueDiffs.get(i),
2927             auDist[tempListIndices.get(i)].index()));
2928       }
2929     }
2930   }
2931 
2932   /**
2933    * Pair species between two crystals based on distances between centers.
2934    *
2935    * @param desiredAUs    Number of pairs to determine.
2936    * @param comparisonNum Determine which centers of mass to use for first crystal.
2937    */
2938   private void pairEntities(int desiredAUs, int comparisonNum) {
2939     tempListIndices.clear();
2940     // List of indexes for second system.
2941     for (DoubleIndexPair doubleIndexPair : targetAUDist_2) {
2942       // Only search molecules within range of the desired number of molecules.
2943       // Must have enough molecules for matching (using exhaustive till better heuristic is determined)
2944       tempListIndices.add(doubleIndexPair.index());
2945     }
2946 
2947     // Compare distances between center of masses from system 1 and 2.
2948     for (int i = 0; i < desiredAUs; i++) {
2949       double minDist = Double.MAX_VALUE;
2950       Integer minIndex = -1;
2951       final double[] baseCoMCurr = baseCoM[comparisonNum][baseAUDist_2[i].index()];
2952       for (Integer target : tempListIndices) {
2953         final double dist = dist2(baseCoMCurr, targetCoM[target]);
2954         if (dist < minDist) {
2955           minIndex = target;
2956           minDist = dist;
2957         }
2958         if (abs(minDist) < MATCH_TOLERANCE) {
2959           // Distance between center of masses is ~0 is the best scenario assuming no coordinate overlaps.
2960           if (logger.isLoggable(Level.FINEST)) {
2961             stringBuilder.append("\n \tExit out of match loop.\n");
2962           }
2963           break;
2964         }
2965       }
2966       pairedAUs[i] = new DoubleIndexPair(minIndex, minDist);
2967       if (!tempListIndices.remove(minIndex)) {
2968         logger.warning(format(" Index value of %d was not found (%4.4f).", minIndex, minDist));
2969       }
2970       if (logger.isLoggable(Level.FINEST)) {
2971         stringBuilder.append(format("\n Base position:   %d: %8.4f %8.4f %8.4f\n", i,
2972             baseCoM[comparisonNum][baseAUDist_2[i].index()][0],
2973             baseCoM[comparisonNum][baseAUDist_2[i].index()][1],
2974             baseCoM[comparisonNum][baseAUDist_2[i].index()][2]));
2975         stringBuilder.append(
2976             format(" Match Distance:  %d: %8.4f\n", i, sqrt(pairedAUs[i].doubleValue())));
2977         int pairedIndex = pairedAUs[i].index();
2978         stringBuilder.append(
2979             format(" Target position: %d: %8.4f %8.4f %8.4f\n", i, targetCoM[pairedIndex][0],
2980                 targetCoM[pairedIndex][1], targetCoM[pairedIndex][2]));
2981       }
2982     }
2983     if (logger.isLoggable(Level.FINER)) {
2984       stringBuilder.append("  Distance between pairs:\n"
2985           + " Index  Base Index  Target Index    Match Index    Distance\n");
2986       for (int i = 0; i < desiredAUs; i++) {
2987         stringBuilder.append(format(" %5d %10d %14d %14d %10.4f\n", i, baseAUDist_2[i].index(),
2988             targetAUDist_2[i].index(), pairedAUs[i].index(), sqrt(pairedAUs[i].doubleValue())));
2989       }
2990     }
2991   }
2992 
2993   /**
2994    * Print values for the symmetry operations performed so far (useful for debugging printSym flag).
2995    *
2996    * @param compareAtomsSize Number of atoms being compared from each AU.
2997    * @param file             File to save current coordinates
2998    * @param name             Description for the current symmetry operator
2999    * @param bondList         List of bonds for saving in XYZ format
3000    * @param atoms            List of atoms for saving XYZ format
3001    * @param forceField       Force field for saving in XYZ format
3002    * @param saveClusters             Save integer switch to determine if/how to save file.
3003    * @param currZ2           Documentation for which molecule from asymmetric unit is being used
3004    */
3005   private void printSym(int compareAtomsSize, File file, String name, List<Bond> bondList,
3006                         Atom[] atoms, ForceField forceField, int saveClusters, int currZ2) {
3007     // Apply inverse of base operations:
3008     // For orthogonal matrices the inverse matrix = the transpose. True iff det(A)== +/- 1.
3009     // No inverse if det(A)==0.
3010     final double[][] tempTransform = new double[4][4];
3011     copyArrayValues(tempTransform, targetTransformSymOp.asMatrix());
3012     addTranslation(targetSymOp.tr, tempTransform, true);
3013     addRotation(targetSymOp.rot, tempTransform, true);
3014     final double[] bestTranslation = new double[]{tempTransform[0][3] / tempTransform[3][3],
3015         tempTransform[1][3] / tempTransform[3][3], tempTransform[2][3] / tempTransform[3][3]};
3016     final SymOp symOp = new SymOp(copyOf(tempTransform, 3), bestTranslation);
3017     final double[] newMol = new double[compareAtomsSize * 3];
3018     StringBuilder printString = new StringBuilder();
3019     String title = format("\n Print Current Symmetry Operator (Z'=%2d):\n"
3020         + " \t Original Coords \t\t  Current Coords \t==\t Applied Sym Op Coords", currZ2);
3021     final double[] originalAU = targetAUoriginal[currZ2];
3022     for (int i = 0; i < compareAtomsSize; i++) {
3023       final int k = i * 3;
3024       final double[] xyz = new double[]{originalAU[k], originalAU[k + 1], originalAU[k + 2]};
3025       applyCartesianSymOp(xyz, xyz, symOp);
3026       newMol[k] = xyz[0];
3027       newMol[k + 1] = xyz[1];
3028       newMol[k + 2] = xyz[2];
3029       printString.append(
3030           format("\n %9.3f %9.3f %9.3f to %9.3f %9.3f %9.3f to %9.3f %9.3f %9.3f", originalAU[k],
3031               originalAU[k + 1], originalAU[k + 2], targetAU[k], targetAU[k + 1], targetAU[k + 2],
3032               newMol[k], newMol[k + 1], newMol[k + 2]));
3033     }
3034     title += format("    RMSD: %9.3f (should be 0.000)", rmsd(newMol, targetAU, massN));
3035     stringBuilder.append(title).append(printString);
3036     saveAssembly(file, name, bondList, atoms, forceField, newMol, comparisonAtoms, 1, "_moveMethod",
3037         0, saveClusters);
3038     stringBuilder.append("\n").append(symOp).append("\n");
3039   }
3040 
3041   /**
3042    * Prioritize asymmetric units within the system based on distance to specified index.
3043    *
3044    * @param coordsXYZ      Coordinates for expanded crystal (should contain 3 * nAtoms * nMols
3045    *                       entries).
3046    * @param centerOfMasses Center of masses for each replicate within inflated crystal.
3047    * @param nAtoms         Number of coordinates in an entity.
3048    * @param auDist         Prioritization of AUs in expanded system based on linkage criteria
3049    * @param index          Index of AU to be center.
3050    * @param linkage        User specified criteria to determine prioritization.
3051    */
3052   private static void prioritizeReplicates(final double[] coordsXYZ, final double[][] centerOfMasses, final int nAtoms,
3053                                            final DoubleIndexPair[] auDist, final int index, final int linkage) {
3054     // Find AU to be treated as the new center.
3055     // AUs added to system based on distance to center of all atoms. Index = 0 AU should be closest to all atom center.
3056     final int nCoords = nAtoms * 3;
3057     final int length = coordsXYZ.length;
3058     final int nMols = length / nCoords;
3059     if (auDist.length != nMols) {
3060       logger.warning(" Number of molecules does not match distance sort array length.");
3061     }
3062     if (linkage == 0) {
3063       // Prioritize based on closest atomic distances (Single Linkage).
3064       final int centerIndex = index * nCoords;
3065       for (int i = 0; i < nMols; i++) {
3066         double tempDist = Double.MAX_VALUE;
3067         final int molIndex = i * nCoords;
3068         for (int j = 0; j < nAtoms; j++) {
3069           final int centerAtomIndex = centerIndex + j * 3;
3070           final double[] centerXYZ = {coordsXYZ[centerAtomIndex], coordsXYZ[centerAtomIndex + 1],
3071               coordsXYZ[centerAtomIndex + 2]};
3072           for (int k = 0; k < nAtoms; k++) {
3073             final int atomIndex = molIndex + k * 3;
3074             final double[] xyz = {coordsXYZ[atomIndex], coordsXYZ[atomIndex + 1],
3075                 coordsXYZ[atomIndex + 2]};
3076             final double currDist = dist2(centerXYZ, xyz);
3077             if (currDist < tempDist) {
3078               tempDist = currDist;
3079             }
3080             if (abs(tempDist) < MATCH_TOLERANCE) {
3081               break;
3082             }
3083           }
3084           if (abs(tempDist) < MATCH_TOLERANCE) {
3085             break;
3086           }
3087         }
3088         auDist[i] = new DoubleIndexPair(i, tempDist);
3089       }
3090       // Sort so the smallest distance is at position 0.
3091       sort(auDist);
3092 
3093     } else if (linkage == 1) {
3094       // Prioritize based on geometric center/center of mass (Average Linkage)
3095       final double[] coordCenter = centerOfMasses[index];
3096       for (int i = 0; i < nMols; i++) {
3097         final double[] moleculeCenter = centerOfMasses[i];
3098         auDist[i] = new DoubleIndexPair(i, dist2(coordCenter, moleculeCenter));
3099       }
3100       // Reorder based on distance to AU closest to Index.
3101       sort(auDist);
3102 
3103       // Molecules in crystal sorted based on distance to center of mass of center most molecule
3104       // Want the first two molecules chosen in this manner, but third molecule to be closest to both
3105       // Assigning distance from auDist ensures correct ordering of center most molecule.
3106       final DoubleIndexPair[] molDists = new DoubleIndexPair[nMols];
3107       int auIndex0 = auDist[0].index();
3108       final int auIndex1 = auDist[1].index();
3109       molDists[0] = new DoubleIndexPair(auIndex0, auDist[0].doubleValue());
3110 
3111       double[] avgCenter = new double[3];
3112       avgCenter[0] = (centerOfMasses[auIndex0][0] + centerOfMasses[auIndex1][0]) / 2;
3113       avgCenter[1] = (centerOfMasses[auIndex0][1] + centerOfMasses[auIndex1][1]) / 2;
3114       avgCenter[2] = (centerOfMasses[auIndex0][2] + centerOfMasses[auIndex1][2]) / 2;
3115 
3116       for (int i = 1; i < nMols; i++) {
3117         auIndex0 = auDist[i].index();
3118         final double[] moleculeCenter = centerOfMasses[auIndex0];
3119         molDists[i] = new DoubleIndexPair(auIndex0, dist2(avgCenter, moleculeCenter));
3120       }
3121       sort(molDists);
3122       // Molecules in crystal sorted based on distance to center of mass of center 2 most molecule
3123       // Want the first three molecules chosen in this manner, but rest to be closest to all three
3124       // Assigning distance from molDists ensures correct ordering of center most molecule.
3125       final DoubleIndexPair[] molDists3 = new DoubleIndexPair[nMols];
3126       molDists3[0] = new DoubleIndexPair(molDists[0].index(), molDists[0].doubleValue());
3127       molDists3[1] = new DoubleIndexPair(molDists[1].index(), molDists[1].doubleValue());
3128       avgCenter = new double[3];
3129       final int bound = min(3, molDists.length);
3130       for (int i = 0; i < bound; i++) {
3131         final int molIndex = molDists[i].index();
3132         avgCenter[0] += centerOfMasses[molIndex][0];
3133         avgCenter[1] += centerOfMasses[molIndex][1];
3134         avgCenter[2] += centerOfMasses[molIndex][2];
3135       }
3136       for (int i = 0; i < 3; i++) {
3137         avgCenter[i] /= 3;
3138       }
3139 
3140       for (int i = 2; i < nMols; i++) {
3141         auIndex0 = molDists[i].index();
3142         final double[] moleculeCenter = centerOfMasses[auIndex0];
3143         molDists3[i] = new DoubleIndexPair(auIndex0, dist2(avgCenter, moleculeCenter));
3144       }
3145       //Reorder based on center point between center-most AU to all atom center and closest AU to center-most AU.
3146       sort(molDists3);
3147       arraycopy(molDists3, 0, auDist, 0, nMols);
3148     } else if (linkage == 2) {
3149       // Prioritize based on minimum distance between the farthest atom (Complete Linkage).
3150       final int centerIndex = index * nCoords;
3151       for (int i = 0; i < nMols; i++) {
3152         double tempDist = 0.0;
3153         final int molIndex = i * nCoords;
3154         for (int j = 0; j < nAtoms; j++) {
3155           final int centerAtomIndex = centerIndex + j * 3;
3156           final double[] centerXYZ = {coordsXYZ[centerAtomIndex], coordsXYZ[centerAtomIndex + 1],
3157               coordsXYZ[centerAtomIndex + 2]};
3158           for (int k = 0; k < nAtoms; k++) {
3159             final int atomIndex = molIndex + k * 3;
3160             final double[] xyz = {coordsXYZ[atomIndex], coordsXYZ[atomIndex + 1],
3161                 coordsXYZ[atomIndex + 2]};
3162             final double currDist = dist2(centerXYZ, xyz);
3163             if (currDist > tempDist) {
3164               tempDist = currDist;
3165             }
3166             if (abs(tempDist) < MATCH_TOLERANCE) {
3167               break;
3168             }
3169           }
3170           if (abs(tempDist) < MATCH_TOLERANCE) {
3171             break;
3172           }
3173         }
3174         auDist[i] = new DoubleIndexPair(i, tempDist);
3175       }
3176       // Sort so the smallest distance is at position 0.
3177       sort(auDist);
3178     } else {
3179       logger.severe(format(" Linkage value of %d was unrecognized. Try 0 (, 1, or 2.", linkage));
3180     }
3181   }
3182 
3183   /**
3184    * Read in the distance matrix.
3185    *
3186    * @param filename The PAC RMSD matrix file to read from.
3187    * @return Stats for all read in distance matrix values.
3188    */
3189   private RunningStatistics readMatrix(final String filename) {
3190     restartRow = 0;
3191     restartColumn = 0;
3192 
3193     final DistanceMatrixFilter distanceMatrixFilter = new DistanceMatrixFilter();
3194     final RunningStatistics runningStatistics = distanceMatrixFilter.readDistanceMatrix(filename, baseSize,
3195         targetSize);
3196 
3197     if (runningStatistics != null && runningStatistics.getCount() > 0) {
3198       restartRow = distanceMatrixFilter.getRestartRow();
3199       restartColumn = distanceMatrixFilter.getRestartColumn();
3200 
3201       if (isSymmetric) {
3202         // Only the diagonal entry (0.0) is on the last row for a symmetric matrix.
3203         if (restartRow == baseSize && restartColumn == 1) {
3204           logger.info(format(" Complete symmetric distance matrix found (%d x %d).", restartRow,
3205               restartRow));
3206         } else {
3207           restartColumn = 0;
3208           logger.info(format(
3209               " Incomplete symmetric distance matrix found.\n Restarting at row %d, column %d.",
3210               restartRow + 1, restartColumn + 1));
3211         }
3212       } else if (restartRow == baseSize && restartColumn == targetSize) {
3213         logger.info(format(" Complete distance matrix found (%d x %d).", restartRow, restartColumn));
3214       } else {
3215         restartColumn = 0;
3216         logger.info(format(" Incomplete distance matrix found.\n Restarting at row %d, column %d.",
3217             restartRow + 1, restartColumn + 1));
3218       }
3219     }
3220 
3221     return runningStatistics;
3222   }
3223 
3224   /**
3225    * Reduce asymmetric unit to atoms that are going to be used in final RMSD.
3226    *
3227    * @param atoms           Atoms we wish to reduce.
3228    * @param comparisonAtoms Atoms of interest within asymmetric unit.
3229    * @return Linear coordinates for only atoms of interest.
3230    */
3231   private static double[] reduceSystem(final Atom[] atoms, final int[] comparisonAtoms) {
3232     // Collect asymmetric unit atomic coordinates.
3233     final double[] reducedCoords = new double[comparisonAtoms.length * 3];
3234     int coordIndex = 0;
3235     for (Integer value : comparisonAtoms) {
3236       final Atom atom = atoms[value];
3237       reducedCoords[coordIndex++] = atom.getX();
3238       reducedCoords[coordIndex++] = atom.getY();
3239       reducedCoords[coordIndex++] = atom.getZ();
3240     }
3241     return reducedCoords;
3242   }
3243 
3244   /**
3245    * Save the provided coordinates as a PDB file.
3246    *
3247    * @param file            File to save coordinates
3248    * @param name            Desired name for file
3249    * @param bondList        List of bonds for saving in XYZ format
3250    * @param forceField0     Force field for saving current assembly
3251    * @param coords          Coordinates to be saved within the PDB
3252    * @param comparisonAtoms Atoms of interest within the initial asymmetric unit.
3253    * @param nAU             Number of desired asymmetric units in comparison
3254    * @param description     Unique identifier that will be added to PDB file name.
3255    * @param compNum         Unique number for the current comparison
3256    * @param saveClusters            Type of file to save (0=none, 1=PDB, 2=XYZ)
3257    */
3258   private void saveAssembly(final File file, String name, final List<Bond> bondList, final Atom[] atoms,
3259                             final ForceField forceField0, final double[] coords, final int[] comparisonAtoms, final int nAU,
3260                             final String description, final int compNum, final int saveClusters) {
3261     //TODO: Save systems out as original molecule regardless of selection
3262     final String fileName = FilenameUtils.removeExtension(file.getName());
3263     File saveLocation;
3264     if (saveClusters == 2) {
3265       saveLocation = new File(fileName + description + ".xyz");
3266     } else if (saveClusters == 1) {
3267       saveLocation = new File(fileName + description + ".pdb");
3268     } else {
3269       return;
3270     }
3271     // Save aperiodic system of the n_mol the closest atoms for visualization.
3272     final MolecularAssembly currentAssembly = new MolecularAssembly(name);
3273     final ArrayList<Atom> newAtomList = new ArrayList<>();
3274     int atomIndex = 1;
3275     final int nCoords = coords.length / (nAU);
3276     // Reset atom Index for indexing new atoms
3277     try { // Assumes same atom selection per AU in Z'>1. Catch exceptions for now.
3278       for (int n = 0; n < nAU; n++) {
3279         // Obtain atoms from moved AU (create copy to move to origin)
3280         // move original and copy to origin
3281         // rotate original to match copy
3282         // translate back to moved location
3283         // Add atoms from moved original to atom list.
3284         final ArrayList<Atom> atomList = new ArrayList<>();
3285         // Create a new set of Atoms for each SymOp molecule
3286         int atomValue = 0;
3287         //Add atoms from comparison to output assembly.
3288         // Assumes same selection between au when Z'>1...
3289         // TODO make work for co-crystals.
3290         final int[] atomSelection = new int[nCoords / 3];
3291         arraycopy(comparisonAtoms, 0, atomSelection, 0, nCoords / 3);
3292         for (final Integer i : atomSelection) {
3293           final Atom a = atoms[i];
3294 
3295           final double[] xyz = new double[3];
3296           final int coordIndex = n * nCoords + atomValue;
3297           xyz[0] = coords[coordIndex];
3298           xyz[1] = coords[coordIndex + 1];
3299           xyz[2] = coords[coordIndex + 2];
3300           final Atom atom = new Atom(atomIndex++, a.getName(), a.getAtomType(), xyz);
3301           atomList.add(atom);
3302           atomValue += 3;
3303         }
3304         // Setup bonds for AUs in comparison.
3305         if (saveClusters == 2) {
3306           //TODO make more robust. Currently only handles all atom selection. Not subsets.
3307           for (final Bond bond : bondList) {
3308             final Atom a1 = bond.getAtom(0);
3309             final Atom a2 = bond.getAtom(1);
3310             //Indices stored as human-readable.
3311             final int a1Ind = a1.getIndex() - 1;
3312             final int a2Ind = a2.getIndex() - 1;
3313             if (IntStream.of(comparisonAtoms).anyMatch(x -> x == a1Ind) && IntStream.of(
3314                 comparisonAtoms).anyMatch(x -> x == a2Ind)) {
3315               final Atom newA1 = atomList.get(a1Ind);
3316               final Atom newA2 = atomList.get(a2Ind);
3317               final Bond b = new Bond(newA1, newA2);
3318               b.setBondType(bond.getBondType());
3319             }
3320           }
3321         }
3322         newAtomList.addAll(atomList);
3323       }
3324     } catch (Exception exception) {
3325       stringBuilder.append("\n Error saving moved coordinates to PDB.\n").append(exception).append("\n");
3326     }
3327 
3328     if (logger.isLoggable(Level.FINEST)) {
3329       final int newSize = newAtomList.size();
3330       stringBuilder.append(format("\n Save Num AU: %3d", nAU));
3331       stringBuilder.append(format("\n Save Num Active: %3d", nCoords));
3332       stringBuilder.append(format("\n Save Num Coords: %3d", coords.length));
3333       stringBuilder.append(format("\n Save Assembly Length: %3d", newSize));
3334       if (newSize > 0) {
3335         final Atom zero = newAtomList.get(0);
3336         stringBuilder.append(
3337             format("\n Save Assembly First Atom: %3s %9.3f %9.3f %9.3f\n", zero.getName(),
3338                 zero.getX(), zero.getY(), zero.getZ()));
3339       } else {
3340         stringBuilder.append("\n WARNING: New list containing new atoms was empty.\n");
3341       }
3342     }
3343 
3344     // Construct the force field for the expanded set of molecules
3345     final ForceField forceField = new ForceField(forceField0.getProperties());
3346 
3347     // Clear all periodic boundary keywords to achieve an aperiodic system.
3348     forceField.clearProperty("a-axis");
3349     forceField.clearProperty("b-axis");
3350     forceField.clearProperty("c-axis");
3351     forceField.clearProperty("alpha");
3352     forceField.clearProperty("beta");
3353     forceField.clearProperty("gamma");
3354     forceField.clearProperty("spacegroup");
3355     currentAssembly.setForceField(forceField);
3356 
3357     // The biochemistry method is designed to load chemical entities into the
3358     // Polymer, Molecule, Water and Ion data structure.
3359     Utilities.biochemistry(currentAssembly, newAtomList);
3360 
3361     currentAssembly.setFile(file);
3362     if (saveClusters == 2) {
3363       final File key = new File(fileName + ".key");
3364       final File properties = new File(fileName + ".properties");
3365       if (key.exists()) {
3366         final File keyComparison = new File(fileName + description + ".key");
3367         try {
3368           if (keyComparison.createNewFile()) {
3369             FileUtils.copyFile(key, keyComparison);
3370           } else {
3371             stringBuilder.append("\n Could not create properties file.\n");
3372           }
3373         } catch (Exception ex) {
3374           // Likely using properties file.
3375           if (logger.isLoggable(Level.FINER)) {
3376             stringBuilder.append(ex);
3377           }
3378         }
3379       } else if (properties.exists()) {
3380         final File propertiesComparison = new File(fileName + description + ".properties");
3381         try {
3382           if (propertiesComparison.createNewFile()) {
3383             FileUtils.copyFile(properties, propertiesComparison);
3384           } else {
3385             stringBuilder.append("\n Could not create properties file.\n");
3386           }
3387         } catch (Exception ex) {
3388           // Likely not using a key/properties file... so PDB?
3389           stringBuilder.append(
3390               "\n Neither key nor properties file detected therefore only creating XYZ.\n");
3391           if (logger.isLoggable(Level.FINER)) {
3392             stringBuilder.append(ex);
3393           }
3394         }
3395       }
3396       final XYZFilter xyzFilter = new XYZFilter(saveLocation, currentAssembly, forceField,
3397           currentAssembly.getProperties());
3398       xyzFilter.writeFile(saveLocation, true);
3399     } else {
3400       final PDBFilter pdbfilter = new PDBFilter(saveLocation, currentAssembly, forceField,
3401           currentAssembly.getProperties());
3402       pdbfilter.setModelNumbering(compNum);
3403       pdbfilter.writeFile(saveLocation, true, false, false);
3404     }
3405     currentAssembly.destroy();
3406   }
3407 
3408   /**
3409    * Save the provided coordinates as a PDB file with accompanying CSV containing RMSD.
3410    *
3411    * @param coords          Coordinates to be saved within the PDB.
3412    * @param comparisonAtoms Atoms of interest within the initial asymmetric unit.
3413    * @param description     Unique identifier that will be added to PDB file name.
3414    * @param finalRMSD       RMSD to be saved to CSV file.
3415    * @param compNum         Unique number for the current comparison
3416    * @param saveClusters            Type of file to save (0=none, 1=PDB, 2=XYZ)
3417    */
3418   private void saveAssembly(final File file, final String name, final List<Bond> bondList, final Atom[] atoms,
3419                             final ForceField forceField, double[] coords, final int[] comparisonAtoms, final int nAU, final String description,
3420                             final double finalRMSD, final int compNum, final int saveClusters) {
3421     saveAssembly(file, name, bondList, atoms, forceField, coords, comparisonAtoms, nAU, description,
3422         compNum, saveClusters);
3423     final String fileName = FilenameUtils.removeExtension(file.getName());
3424     try {
3425       // Needs same name as PDB so follow PDB format.
3426       File csv = PDBFilter.version(new File(fileName + description + ".csv"));
3427       if (csv.createNewFile()) {
3428         BufferedWriter bw = new BufferedWriter(new FileWriter(csv, false));
3429         bw.append("rms\n");
3430         bw.append(format("%4.4f\n", finalRMSD));
3431         bw.close();
3432       } else {
3433         logger.warning(" Could not create CSV file \"" + csv.getName() + "\"");
3434       }
3435     } catch (Exception ex) {
3436       logger.info(ex.toString());
3437     }
3438   }
3439 }
3440 
3441 // Used for LMN expansion. Computes ratio between a, b, c axis lengths to guess final L x M x N distribution.
3442 //  /**
3443 //   * FFX Replicates crystal has to obey lattice requirements. Update LMN accordingly.
3444 //   * @param lattice Lattice system of the current crystal.
3445 //   * @param lmn     Proposed LMN values (will be updated to satisfy lattice requirements).
3446 //   */
3447 //  private static void checkLattice(LatticeSystem lattice, int[] lmn){
3448 //    int restriction;
3449 //    switch(lattice) {
3450 //      case HEXAGONAL_LATTICE:
3451 //      case TETRAGONAL_LATTICE:
3452 //        //a==b
3453 //        restriction = 0;
3454 //        break;
3455 //      case CUBIC_LATTICE:
3456 //      case RHOMBOHEDRAL_LATTICE:
3457 //        //a==b==c
3458 //        restriction = 1;
3459 //        break;
3460 //      default:
3461 //        //no restriction
3462 //        restriction = -1;
3463 //        break;
3464 //    }
3465 //    if(restriction == 0){
3466 //      if(lmn[0] != lmn[1]){
3467 //        int max = max(lmn[0],lmn[1]);
3468 //        lmn[0] = max;
3469 //        lmn[1] = max;
3470 //      }
3471 //    }else if(restriction == 1){
3472 //      if(lmn[0] != lmn[2] || lmn[1] != lmn[2]){
3473 //        int max = max(lmn[0],max(lmn[1],lmn[2]));
3474 //        lmn[0] = max;
3475 //        lmn[1] = max;
3476 //        lmn[2] = max;
3477 //      }
3478 //    }
3479 //  }
3480 //    // Used in LMN expansion as well.
3481 //  /**
3482 //   * Determine replicates expansion based on the size of molecules contained in unit cell.
3483 //   *
3484 //   * @param unitCell        Unit cell to be expanded.
3485 //   * @param reducedCoords           Atoms in system.
3486 //   * @param comparisonAtoms Atoms active in comparison.
3487 //   * @param nAU             Number of asymmetric units to be compared.
3488 //   * @param scaleFactor     Scaling factor to determine final size (default nAU * 10).
3489 //   * @return LMN values for replicates crystal.
3490 //   */
3491 //  private static int[] determineExpansion(final Crystal unitCell, final double[] reducedCoords, final int[] comparisonAtoms, final double[] mass,
3492 //                                          final int nAU, final int zPrime, final double scaleFactor) {
3493 //    // TODO Create better expansion criteria (hopefully can eliminate checkInflatedSphere method some day...).
3494 //    final int XX = 0;
3495 //    final int YY = 1;
3496 //    final int ZZ = 2;
3497 //    // Determine number of symmetry operators in unit cell.
3498 //    final int nSymm = unitCell.spaceGroup.getNumberOfSymOps();
3499 //    final int numEntities = nSymm * zPrime;
3500 //    final int compareAtomsSize = comparisonAtoms.length;
3501 //
3502 //    int zAtoms = compareAtomsSize / zPrime;
3503 //    // Collect asymmetric unit atomic coordinates.
3504 //    double[] x = new double[compareAtomsSize];
3505 //    double[] y = new double[compareAtomsSize];
3506 //    double[] z = new double[compareAtomsSize];
3507 //    for (int i = 0; i < compareAtomsSize; i++) {
3508 //      int atomIndex = i * 3;
3509 //      x[i] = reducedCoords[atomIndex];
3510 //      y[i] = reducedCoords[atomIndex + 1];
3511 //      z[i] = reducedCoords[atomIndex + 2];
3512 //    }
3513 //
3514 //    // Symmetry coordinates for each molecule in replicates crystal
3515 //    double[][] xS = new double[nSymm][compareAtomsSize];
3516 //    double[][] yS = new double[nSymm][compareAtomsSize];
3517 //    double[][] zS = new double[nSymm][compareAtomsSize];
3518 //
3519 //    // Loop over replicate crystal SymOps
3520 //    List<SymOp> inflatedSymOps = unitCell.spaceGroup.symOps;
3521 //    for (int iSym = 0; iSym < nSymm; iSym++) {
3522 //      SymOp symOp = inflatedSymOps.get(iSym);
3523 //      //Convert sym op into cartesian for later use.
3524 //      double[][] rot = new double[3][3];
3525 //      unitCell.getTransformationOperator(symOp, rot);
3526 //      // Apply SymOp to the asymmetric unit reducedCoords Cartesian Coordinates.
3527 //      unitCell.applySymOp(compareAtomsSize, x, y, z, xS[iSym], yS[iSym], zS[iSym], symOp);
3528 //      for (int zp = 0; zp < zPrime; zp++) {
3529 //        int symIndex = zp * zAtoms;
3530 //        // Compute center-of-mass (CoM) for Cartesian coordinates
3531 //        double[] centerOfMass = new double[3];
3532 //        double totalMass = 0.0;
3533 //        for (int i = 0; i < zAtoms; i++) {
3534 //          double m = mass[i];
3535 //          int coordIndex = symIndex + i;
3536 //          centerOfMass[0] += xS[iSym][coordIndex] * m;
3537 //          centerOfMass[1] += yS[iSym][coordIndex] * m;
3538 //          centerOfMass[2] += zS[iSym][coordIndex] * m;
3539 //          totalMass += m;
3540 //        }
3541 //        centerOfMass[0] /= totalMass;
3542 //        centerOfMass[1] /= totalMass;
3543 //        centerOfMass[2] /= totalMass;
3544 //
3545 //        double[] translate = moveIntoCrystal(unitCell, centerOfMass);
3546 //        //Convert sym op into cartesian for later use.
3547 //        double[] trans = symOp.tr.clone();
3548 //        unitCell.toCartesianCoordinates(trans, trans);
3549 //        int translateLength = translate.length;
3550 //        for (int i = 0; i < translateLength; i++) {
3551 //          trans[i] += translate[i];
3552 //        }
3553 //        for (int i = 0; i < zAtoms; i++) {
3554 //          int coordIndex = symIndex + i;
3555 //          xS[iSym][coordIndex] += translate[0];
3556 //          yS[iSym][coordIndex] += translate[1];
3557 //          zS[iSym][coordIndex] += translate[2];
3558 //        }
3559 //      }
3560 //    }
3561 //
3562 //    // Loop over molecule(s) in unit cell to determine maximum dimensions of unit cell.
3563 //    double maxDistX = 0.0;
3564 //    double maxDistY = 0.0;
3565 //    double maxDistZ = 0.0;
3566 //    for (int i = 0; i < nSymm; i++) {
3567 //      double maxSymDistX = 0.0;
3568 //      double maxSymDistY = 0.0;
3569 //      double maxSymDistZ = 0.0;
3570 //      for (int j = 0; j < compareAtomsSize; j++) {
3571 //        double x1 = xS[i][j];
3572 //        double y1 = yS[i][j];
3573 //        double z1 = zS[i][j];
3574 //        for (int m = j + 1; m < compareAtomsSize; m++) {
3575 //          double x2 = xS[i][m];
3576 //          double y2 = yS[i][m];
3577 //          double z2 = zS[i][m];
3578 //          double distX = abs(x2 - x1);
3579 //          double distY = abs(y2 - y1);
3580 //          double distZ = abs(z2 - z1);
3581 //          if (distX > maxSymDistX) {
3582 //            maxSymDistX = distX;
3583 //          }
3584 //          if (distY > maxSymDistY) {
3585 //            maxSymDistY = distY;
3586 //          }
3587 //          if (distZ > maxSymDistZ) {
3588 //            maxSymDistZ = distZ;
3589 //          }
3590 //        }
3591 //      }
3592 //      if (maxSymDistX > maxDistX) {
3593 //        maxDistX = maxSymDistX;
3594 //      }
3595 //      if (maxSymDistY > maxDistY) {
3596 //        maxDistY = maxSymDistY;
3597 //      }
3598 //      if (maxSymDistZ > maxDistZ) {
3599 //        maxDistZ = maxSymDistZ;
3600 //      }
3601 //    }
3602 //    // We have calculated max distance between center of atoms. Add wiggle room to prevent overlap.
3603 //    maxDistX += 4;
3604 //    maxDistY += 4;
3605 //    maxDistZ += 4;
3606 //    // Convert X,Y,Z differences to % of a,b,c lattice lengths.
3607 //    double[] output = new double[3];
3608 //    unitCell.toFractionalCoordinates(new double[]{maxDistX, maxDistY, maxDistZ}, output);
3609 //    // Initial guesses for l, m, n.
3610 //    double target = nAU * scaleFactor;
3611 //    output[XX] = ceil(output[XX]);
3612 //    output[YY] = ceil(output[YY]);
3613 //    output[ZZ] = ceil(output[ZZ]);
3614 //    double l = cbrt((target * output[XX] * output[XX])/(numEntities * output[YY] * output[ZZ]));
3615 //    double m = cbrt((target * output[YY] * output[YY])/(numEntities * output[XX] * output[ZZ]));
3616 //    double n = cbrt((target * output[ZZ] * output[ZZ])/(numEntities * output[XX] * output[YY]));
3617 //    int[] lmn = new int[]{(int) ceil(l), (int) ceil(m), (int) ceil(n)};
3618 //    // Minimum number of asymmetric units desired in replicates crystal.
3619 //    int total = (lmn[0] * lmn[1] * lmn[2]) * numEntities;
3620 //    int[] adjust = new int[]{0, 0, 0};
3621 //    if (logger.isLoggable(Level.FINER)) {
3622 //      logger.info(format(" Number Sym Ops: %3d", nSymm));
3623 //      logger.info(format(" Fractional Distances: %9.3f %9.3f %9.3f", output[XX], output[YY], output[ZZ]));
3624 //      logger.info(format(" LMN Guess: %3d %3d %3d (Total: %3d > %9.3f)", lmn[0], lmn[1], lmn[2], total, target));
3625 //    }
3626 //    // Increase l,m,n while maintaining ratio between them until target number of structures have been obtained.
3627 //    while (total < target) {
3628 //      // Maintain approximate ratio by enforcing each value has to increase before any increase twice.
3629 //      boolean A = adjust[1] == 1 && adjust[2] == 1;
3630 //      boolean B = adjust[0] == 1 && adjust[2] == 1;
3631 //      boolean C = adjust[0] == 1 && adjust[1] == 1;
3632 //      if (A || B || C) {
3633 //        if (A) {
3634 //          adjust[1] = 0;
3635 //          adjust[2] = 0;
3636 //          lmn[0]++;
3637 //        }
3638 //        if (B) {
3639 //          adjust[0] = 0;
3640 //          adjust[2] = 0;
3641 //          lmn[1]++;
3642 //        }
3643 //        if (C) {
3644 //          adjust[0] = 0;
3645 //          adjust[1] = 0;
3646 //          lmn[2]++;
3647 //        }
3648 //      } else {
3649 //        if (adjust[0] == 0 && lmn[0] < lmn[1]) {
3650 //          if (lmn[0] < lmn[2]) {
3651 //            lmn[0]++;
3652 //            adjust[0]++;
3653 //          } else if (lmn[0] == lmn[2]) {
3654 //            if (output[XX] > output[ZZ]) {
3655 //              lmn[0]++;
3656 //              adjust[0]++;
3657 //            } else if (adjust[2] == 0) {
3658 //              lmn[2]++;
3659 //              adjust[2]++;
3660 //            } else {
3661 //              lmn[0]++;
3662 //              adjust[0]++;
3663 //            }
3664 //          } else {
3665 //            lmn[1]++;
3666 //            adjust[1]++;
3667 //          }
3668 //        } else if (adjust[1] == 0 && lmn[1] < lmn[0]) {
3669 //          if (lmn[1] < lmn[2]) {
3670 //            lmn[1]++;
3671 //            adjust[1]++;
3672 //          } else if (lmn[1] == lmn[2]) {
3673 //            if (output[YY] > output[ZZ]) {
3674 //              lmn[1]++;
3675 //              adjust[1]++;
3676 //            } else if (adjust[2] == 0) {
3677 //              lmn[2]++;
3678 //              adjust[2]++;
3679 //            } else {
3680 //              lmn[1]++;
3681 //              adjust[1]++;
3682 //            }
3683 //          } else {
3684 //            lmn[2]++;
3685 //            adjust[2]++;
3686 //          }
3687 //        } else if (adjust[2] == 0 && lmn[2] < lmn[0]) {
3688 //          lmn[2]++;
3689 //          adjust[2]++;
3690 //        } else {
3691 //          if (adjust[0] == 1) {
3692 //            if (output[YY] >= output[ZZ]) {
3693 //              lmn[1]++;
3694 //              adjust[1]++;
3695 //            } else {
3696 //              lmn[2]++;
3697 //              adjust[2]++;
3698 //            }
3699 //          } else if (adjust[1] == 1) {
3700 //            if (output[XX] >= output[ZZ]) {
3701 //              lmn[0]++;
3702 //              adjust[0]++;
3703 //            } else {
3704 //              lmn[2]++;
3705 //              adjust[2]++;
3706 //            }
3707 //          } else if (adjust[2] == 1) {
3708 //            if (output[XX] >= output[YY]) {
3709 //              lmn[0]++;
3710 //              adjust[0]++;
3711 //            } else {
3712 //              lmn[1]++;
3713 //              adjust[1]++;
3714 //            }
3715 //          } else {
3716 //            if (output[XX] >= output[YY] && output[XX] >= output[ZZ]) {
3717 //              lmn[0]++;
3718 //              adjust[0]++;
3719 //            } else if (output[YY] >= output[XX] && output[YY] >= output[ZZ]) {
3720 //              lmn[1]++;
3721 //              adjust[1]++;
3722 //            } else {
3723 //              lmn[2]++;
3724 //              adjust[2]++;
3725 //            }
3726 //          }
3727 //        }
3728 //      }
3729 //      // TODO determine if below steps are necessary (change replicates expansion method)
3730 //      LatticeSystem lattice = unitCell.spaceGroup.latticeSystem;
3731 //      checkLattice(lattice, lmn);
3732 //      total = (lmn[0] * lmn[1] * lmn[2]) * numEntities;
3733 //      if (logger.isLoggable(Level.FINEST)) {
3734 //        logger.finest(format(" \tLMN Guess: %3d %3d %3d (Total: %3d > %9.3f)", lmn[0], lmn[1], lmn[2], total, target));
3735 //      }
3736 //    }
3737 //    // Want a 1 unit cell buffer on all sides. Must be at least 3 wide in each direction.
3738 //    if(lmn[0] < 3){
3739 //      lmn[0] = 3;
3740 //    }
3741 //    if(lmn[1] < 3){
3742 //      lmn[1] = 3;
3743 //    }
3744 //    if(lmn[2] < 3){
3745 //      lmn[2] = 3;
3746 //    }
3747 //    if (logger.isLoggable(Level.FINE)) {
3748 //      logger.fine(format(" Final LMN: %3d %3d %3d (Total: %3d > %9.3f)", lmn[0], lmn[1], lmn[2], total, target));
3749 //    }
3750 //
3751 //    //Return l,m,n for desired replicates crystal.
3752 //    return new int[]{lmn[0], lmn[1], lmn[2]};
3753 //  }
3754 
3755 //        // Used in QEtoXYZ.groovy which is not ready for git which is why this method appears unused.
3756 //    /**
3757 //     * Orient coordinates so that the second index is on the x axis, and the third index is on the X-Y
3758 //     * plane. First index should be at the origin (0, 0, 0).
3759 //     *
3760 //     * @param coordsXYZ   An array of XYZ positions (e.g. [x0, y0, z0, x1, y1, z1, x2, y2, z2]
3761 //     * @param atomIndices Indices for three desired sets from the XYZ list (e.g. [0, 1, 2]). Index
3762 //     *                    0 should be at origin.
3763 //     */
3764 //    public static void standardOrientation(double[] coordsXYZ, int[] atomIndices) {
3765 //        int numCoords = coordsXYZ.length / 3;
3766 //        double[] atomCoords = new double[3 * 3];
3767 //        atomCoords[0] = coordsXYZ[atomIndices[0]];
3768 //        atomCoords[1] = coordsXYZ[atomIndices[0] + 1];
3769 //        atomCoords[2] = coordsXYZ[atomIndices[0] + 2];
3770 //        atomCoords[3] = coordsXYZ[atomIndices[1]];
3771 //        atomCoords[4] = coordsXYZ[atomIndices[1] + 1];
3772 //        atomCoords[5] = coordsXYZ[atomIndices[1] + 2];
3773 //        atomCoords[6] = coordsXYZ[atomIndices[2]];
3774 //        atomCoords[7] = coordsXYZ[atomIndices[2] + 1];
3775 //        atomCoords[8] = coordsXYZ[atomIndices[2] + 2];
3776 //
3777 //        // TODO: Delete coordsXYZOrig?
3778 //        double[] coordsXYZOrig = new double[numCoords * 3];
3779 //        for (int i = 0; i < numCoords; i++) {
3780 //            int atomIndex = i * 3;
3781 //            coordsXYZOrig[atomIndex] = coordsXYZ[atomIndex];
3782 //            coordsXYZOrig[atomIndex + 1] = coordsXYZ[atomIndex + 1];
3783 //            coordsXYZOrig[atomIndex + 2] = coordsXYZ[atomIndex + 2];
3784 //        }
3785 //
3786 //        // TODO: Delete atomsCoordsOrig?
3787 //        double[] atomsCoordsOrig = new double[3 * 3];
3788 //        arraycopy(atomCoords, 0, atomsCoordsOrig, 0, 9);
3789 //
3790 //        logger.fine(
3791 //                format(" START: N1:\t%16.15f %16.15f %16.15f", atomCoords[0], atomCoords[1], atomCoords[2]));
3792 //        logger.fine(
3793 //                format(" START: N2:\t%16.15f %16.15f %16.15f", atomCoords[3], atomCoords[4], atomCoords[5]));
3794 //        logger.fine(
3795 //                format(" START: N3:\t%16.15f %16.15f %16.15f", atomCoords[6], atomCoords[7], atomCoords[8]));
3796 //
3797 //        double p1n2 = coordsXYZ[atomIndices[1]];
3798 //        double q1n2 = coordsXYZ[atomIndices[1] + 1];
3799 //        double r1n2 = coordsXYZ[atomIndices[1] + 2];
3800 //
3801 //        // Calculation of sigma, phai, and cita angles needed to get specified atoms to desired loci
3802 //        double cita0 = acos(p1n2 / sqrt(p1n2 * p1n2 + q1n2 * q1n2));
3803 //        double phai0 = acos(sqrt(p1n2 * p1n2 + q1n2 * q1n2) /
3804 //                sqrt(p1n2 * p1n2 + q1n2 * q1n2 + r1n2 * r1n2));
3805 //        if (q1n2 < 0.0) {
3806 //            cita0 = -cita0;
3807 //        }
3808 //
3809 //        for (int i = 0; i < numCoords; i++) {
3810 //            int atomIndex = i * 3;
3811 //            double ptmp = coordsXYZ[atomIndex] * cos(cita0) + coordsXYZ[atomIndex + 1] * sin(cita0);
3812 //            double qtmp = -coordsXYZ[atomIndex] * sin(cita0) + coordsXYZ[atomIndex + 1] * cos(cita0);
3813 //            coordsXYZ[atomIndex] = ptmp;
3814 //            coordsXYZ[atomIndex + 1] = qtmp;
3815 //        }
3816 //
3817 //        p1n2 = coordsXYZ[atomIndices[1]];
3818 //        q1n2 = coordsXYZ[atomIndices[1] + 1];
3819 //
3820 //        if (r1n2 > 0.0) {
3821 //            phai0 = -phai0;
3822 //        }
3823 //
3824 //        for (int i = 0; i < numCoords; i++) {
3825 //            int atomIndex = i * 3;
3826 //            double ptmp = coordsXYZ[atomIndex] * cos(phai0) - coordsXYZ[atomIndex + 2] * sin(phai0);
3827 //            double rtmp = coordsXYZ[atomIndex] * sin(phai0) + coordsXYZ[atomIndex + 2] * cos(phai0);
3828 //            coordsXYZ[atomIndex] = ptmp;
3829 //            coordsXYZ[atomIndex + 2] = rtmp;
3830 //        }
3831 //
3832 //        p1n2 = coordsXYZ[atomIndices[1]];
3833 //        r1n2 = coordsXYZ[atomIndices[1] + 2];
3834 //        double p1n3 = coordsXYZ[atomIndices[2]];
3835 //        double q1n3 = coordsXYZ[atomIndices[2] + 1];
3836 //        double r1n3 = coordsXYZ[atomIndices[2] + 2];
3837 //
3838 //        double sigma0 = acos(q1n3 / sqrt(q1n3 * q1n3 + r1n3 * r1n3));
3839 //        if (r1n3 < 0.0) {
3840 //            sigma0 = -sigma0;
3841 //        }
3842 //
3843 //        for (int i = 0; i < numCoords; i++) {
3844 //            int atomIndex = i * 3;
3845 //            double qtmp = coordsXYZ[atomIndex + 1] * cos(sigma0) + coordsXYZ[atomIndex + 2] * sin(sigma0);
3846 //            double rtmp = -coordsXYZ[atomIndex + 1] * sin(sigma0) + coordsXYZ[atomIndex + 2] * cos(sigma0);
3847 //            coordsXYZ[atomIndex + 1] = qtmp;
3848 //            coordsXYZ[atomIndex + 2] = rtmp;
3849 //        }
3850 //
3851 //        q1n2 = coordsXYZ[atomIndices[1] + 1];
3852 //        r1n2 = coordsXYZ[atomIndices[1] + 2];
3853 //        q1n3 = coordsXYZ[atomIndices[2] + 1];
3854 //        r1n3 = coordsXYZ[atomIndices[2] + 2];
3855 //
3856 //        if (logger.isLoggable(Level.FINER)) {
3857 //            logger.finer(
3858 //                    format(" DONE N1: %16.15f %16.15f %16.15f", atomCoords[0], atomCoords[1], atomCoords[2]));
3859 //            logger.finer(format(" DONE N2: %16.15f %16.15f %16.15f", p1n2, q1n2, r1n2));
3860 //            logger.finer(format(" DONE N3: %16.15f %16.15f %16.15f", p1n3, q1n3, r1n3));
3861 //        }
3862 //    }
3863 
3864 //  Frank-Kasper phases of metallic ions can reach a coordination number of 16...