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