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