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.nonbonded;
39  
40  import edu.rit.pj.BarrierAction;
41  import edu.rit.pj.IntegerForLoop;
42  import edu.rit.pj.IntegerSchedule;
43  import edu.rit.pj.ParallelRegion;
44  import edu.rit.pj.ParallelTeam;
45  import edu.rit.pj.reduction.SharedBoolean;
46  import edu.rit.pj.reduction.SharedInteger;
47  import edu.rit.util.Range;
48  import ffx.crystal.Crystal;
49  import ffx.potential.MolecularAssembly;
50  import ffx.potential.bonded.Atom;
51  import ffx.potential.utils.PotentialsUtils;
52  
53  import java.io.File;
54  import java.util.ArrayList;
55  import java.util.Collections;
56  import java.util.HashMap;
57  import java.util.HashSet;
58  import java.util.List;
59  import java.util.Map;
60  import java.util.PriorityQueue;
61  import java.util.Set;
62  import java.util.logging.Level;
63  import java.util.logging.Logger;
64  
65  import static java.lang.String.format;
66  import static java.lang.System.arraycopy;
67  import static java.util.Arrays.copyOf;
68  import static java.util.Arrays.fill;
69  import static org.apache.commons.math3.util.FastMath.floor;
70  import static org.apache.commons.math3.util.FastMath.min;
71  import static org.apache.commons.math3.util.FastMath.sqrt;
72  
73  /**
74   * The NeighborList class builds Verlet lists in parallel via a spatial decomposition. <br>
75   *
76   * <ol>
77   *   <li>The unit cell is partitioned into <code>nA * nB * nC</code> smaller axis-aligned cells,
78   *       where {nA, nB, nC} are chosen as large as possible subject to the criteria that the length
79   *       of each side of a sub-volume (rCellA, rCellB, rCellC) multiplied by nEdge
80   *       must be greater than the cutoff distance <code>Rcut</code> plus a
81   *       buffer distance <code>delta</code>: <br>
82   *       <code>rCellA * nEdge .GE. (Rcut + delta)</code> <br>
83   *       <code>rCellB * nEdge .GE. (Rcut + delta)</code> <br>
84   *       <code>rCellC * nEdge .GE. (Rcut + delta)</code> <br>
85   *       All neighbors of an atom are in a block of (2*nEdge+1)^3 neighborCells.
86   *   <li>Interactions between an atom and neighbors in the asymmetric unit require only half the
87   *       neighboring cells to be searched to avoid double counting. However, enumeration of
88   *       interactions between an atom in the asymmetric unit and its neighbors in a symmetry mate
89   *       require all cells to be searched.
90   *   <li>Verlet lists from the search are stored, which reduces the number of neighbors whose
91   *       distances must be calculated by a factor of approximately: <br>
92   *       <code>(4/3*Pi*Rcut^3)/(neighborCells*Vcell)</code>
93   *       About 1/3 as many interactions are contained in the Verlet lists compared to
94   *       the total amount in the neighboring cells.
95   * </ol>
96   *
97   * @author Michael J. Schnieders
98   * @since 1.0
99   */
100 public class NeighborList extends ParallelRegion {
101 
102   /**
103    * The logger.
104    */
105   private static final Logger logger = Logger.getLogger(NeighborList.class.getName());
106 
107   private final DomainDecomposition domainDecomposition;
108   /**
109    * The cutoff beyond which the pairwise energy is zero.
110    */
111   private final double cutoff;
112   /**
113    * A buffer, which is added to the cutoff distance, such that the Verlet lists do not need to be
114    * calculated for all coordinate changes.
115    */
116   private final double buffer;
117   /**
118    * The maximum squared displacement allowed before list rebuild.
119    */
120   private final double motion2;
121   /**
122    * The sum of the cutoff + buffer.
123    */
124   private final double cutoffPlusBuffer;
125   /**
126    * Total^2 for distance comparisons without taking a sqrt.
127    */
128   private final double cutoffPlusBuffer2;
129   /**
130    * The ParallelTeam coordinates use of threads and their schedules.
131    */
132   private final ParallelTeam parallelTeam;
133   /**
134    * Number of threads used by the parallelTeam.
135    */
136   private final int threadCount;
137   /**
138    * If this flag is set, then the Verlet lists will be rebuilt even in the absence of motion.
139    */
140   private boolean forceRebuild = false;
141   /**
142    * If this flag is set then the Verlet lists will be printed to the logger.
143    */
144   private boolean print = false;
145   /**
146    * Shared Boolean used to detect list rebuild. If any thread detects motion in the MotionLoop, the
147    * value is set to true.
148    */
149   private final SharedBoolean sharedMotion;
150   /**
151    * Detect motion in the asymmetric unit.
152    */
153   private final MotionLoop[] motionLoops;
154   /**
155    * Perform some initialization tasks prior to rebuilding the list.
156    */
157   private final ListInitBarrierAction listInitBarrierAction;
158   /**
159    * Assign atoms to cells in parallel. This loop is executed once per list rebuild.
160    */
161   private final AssignAtomsToCellsLoop[] assignAtomsToCellsLoops;
162   /**
163    * A Verlet list loop for each thread.
164    */
165   private final VerletListLoop[] verletListLoop;
166   private final GroupVerletListLoop[] groupVerletListLoop;
167   /**
168    * Time to check for motion.
169    */
170   private long motionTime;
171   /**
172    * Time for some initializations.
173    */
174   private long initTime;
175   /**
176    * Time to assign atoms to cells.
177    */
178   private long assignAtomsToCellsTime;
179   /**
180    * Time to build the Verlet lists.
181    */
182   private long verletListTime;
183   /**
184    * Time to build groups and associated lists
185    */
186   private long groupingTime;
187   /**
188    * Total number of interactions.
189    */
190   private final SharedInteger sharedCount;
191   /**
192    * Optimal pairwise ranges.
193    */
194   private final Range[] ranges;
195   /**
196    * The crystal object defines the unit cell dimensions and space group. If replicates of the unit
197    * cell are being used, this should be the overall replicates crystal and not the small unit cell.
198    */
199   private Crystal crystal;
200   /**
201    * The number of asymmetric units in the unit cell.
202    */
203   private int nSymm;
204   /**
205    * The number of atoms in the asymmetric unit.
206    */
207   private int nAtoms;
208   /**
209    * Reduced coordinates for each symmetry copy. [nSymm][3*nAtoms]
210    */
211   private double[][] coordinates;
212   /**
213    * The Cartesian coordinates of the asymmetric unit when the list was last rebuilt.
214    */
215   private double[] previous;
216   /**
217    * The Verlet lists. [nSymm][nAtoms][nNeighbors]
218    */
219   private int[][][] lists;
220   /**
221    * Size of each group of atoms
222    */
223   private int M = -1;
224   /**
225    * Groups with M atoms. [group][M]
226    */
227   private int[][] mGroups;
228   /**
229    * Number of groups.
230    */
231   private int numGroups;
232   /**
233    * mGroup lookup for each atom.
234    */
235   private int[] mGroupID;
236   /**
237    * Group-Group interaction lists. [SymOp][num of mGroups][num of mGroup neighbors]
238    */
239   private int[][][] groupLists;
240   /**
241    * Number of interactions per atom.
242    */
243   private int[] listCount;
244   /**
245    * Pairwise ranges for load balancing.
246    */
247   private PairwiseSchedule pairwiseSchedule;
248   /**
249    * Number of interactions between atoms in the asymmetric unit.
250    */
251   private int asymmetricUnitCount;
252   /**
253    * Number of interactions between atoms in the asymmetric unit and atoms in symmetry mates.
254    */
255   private int symmetryMateCount;
256   /**
257    * Number of interactions between groups of atoms in the asymmetric unit.
258    */
259   private int groupAsymmetricUnitCount;
260   /**
261    * Number of interactions between groups of atoms in the asymmetric unit
262    * and groups of atoms in symmetry mates.
263    */
264   private int groupSymmetryMateCount;
265   /**
266    * Atoms being used list.
267    */
268   private boolean[] use;
269   /**
270    * List of atoms.
271    */
272   private Atom[] atoms;
273   /**
274    * Time building the neighbor list.
275    */
276   private long time;
277   /**
278    * Include intermolecular interactions.
279    */
280   private boolean intermolecular = true;
281   /**
282    * If true, interactions between two inactive atoms are included in the Neighborlist. Set to true
283    * to match OpenMM behavior (i.e. interactions between two atoms with zero mass are included). Set
284    * to false for more efficient "pure Java" optimizations.
285    */
286   private final boolean includeInactivePairs = true;
287   /**
288    * Disable updates to the NeighborList; use with caution.
289    */
290   private boolean disableUpdates = false;
291 
292   /**
293    * Constructor for the NeighborList class.
294    *
295    * @param crystal      Definition of the unit cell and space group.
296    * @param atoms        The atoms to generate Verlet lists for.
297    * @param cutoff       The cutoff distance.
298    * @param buffer       The buffer distance.
299    * @param parallelTeam Specifies the parallel environment.
300    * @since 1.0
301    */
302   public NeighborList(Crystal crystal, Atom[] atoms, double cutoff,
303                       double buffer, ParallelTeam parallelTeam) {
304     this.crystal = crystal;
305     this.cutoff = cutoff;
306     this.buffer = buffer;
307     this.parallelTeam = new ParallelTeam(parallelTeam.getThreadCount());
308     this.atoms = atoms;
309     nAtoms = atoms.length;
310 
311     // Configure the neighbor cutoff and list rebuilding criteria.
312     cutoffPlusBuffer = cutoff + buffer;
313     cutoffPlusBuffer2 = cutoffPlusBuffer * cutoffPlusBuffer;
314     motion2 = (buffer / 2.0) * (buffer / 2.0);
315 
316     // Initialize parallel constructs.
317     threadCount = parallelTeam.getThreadCount();
318     sharedCount = new SharedInteger();
319     ranges = new Range[threadCount];
320 
321     sharedMotion = new SharedBoolean();
322     motionLoops = new MotionLoop[threadCount];
323     listInitBarrierAction = new ListInitBarrierAction();
324     assignAtomsToCellsLoops = new AssignAtomsToCellsLoop[threadCount];
325     verletListLoop = new VerletListLoop[threadCount];
326     groupVerletListLoop = new GroupVerletListLoop[threadCount];
327     for (int i = 0; i < threadCount; i++) {
328       motionLoops[i] = new MotionLoop();
329       verletListLoop[i] = new VerletListLoop();
330       assignAtomsToCellsLoops[i] = new AssignAtomsToCellsLoop();
331       groupVerletListLoop[i] = new GroupVerletListLoop();
332     }
333 
334     // Initialize the neighbor list builder subcells.
335     boolean print = logger.isLoggable(Level.FINE);
336     domainDecomposition = new DomainDecomposition(nAtoms, crystal, cutoffPlusBuffer);
337     initNeighborList(print);
338   }
339 
340   /**
341    * This method can be called as necessary to build/rebuild the neighbor lists.
342    *
343    * @param coordinates  The coordinates of each atom [nSymm][nAtoms*3].
344    * @param lists        The neighbor lists [nSymm][nAtoms][nPairs].
345    * @param forceRebuild If true, the list is rebuilt even if no atom has moved half the buffer
346    *                     size.
347    * @param use          an array of boolean.
348    * @param print        a boolean.
349    * @since 1.0
350    */
351   public void buildList(final double[][] coordinates, final int[][][] lists, boolean[] use,
352                         boolean forceRebuild, boolean print) {
353     if (disableUpdates) {
354       return;
355     }
356     this.coordinates = coordinates;
357     this.lists = lists;
358     this.use = use;
359     this.forceRebuild = forceRebuild;
360     this.print = print;
361 
362     try {
363       parallelTeam.execute(this);
364     } catch (Exception e) {
365       String message = "Fatal exception building neighbor list.\n";
366       logger.log(Level.SEVERE, message, e);
367     }
368   }
369 
370   /**
371    * Set the group size for group-based Verlet lists.
372    * <p>
373    * If M is less than or equal to 1, then the NeighborList will not
374    * build group-based Verlet lists.
375    *
376    * @param M The size of the groups to build.
377    */
378   public void setGroupSize(int M) {
379     this.M = M;
380   }
381 
382   /**
383    * destroy.
384    *
385    * @throws java.lang.Exception if any.
386    */
387   public void destroy() throws Exception {
388     parallelTeam.shutdown();
389   }
390 
391   /**
392    * {@inheritDoc}
393    *
394    * <p>This is method should not be called; it is invoked by Parallel Java.
395    *
396    * @since 1.0
397    */
398   @Override
399   public void finish() {
400     if (disableUpdates) {
401       return;
402     }
403     if (!forceRebuild && !sharedMotion.get()) {
404       return;
405     }
406 
407     // Number of atoms in the asymmetric unit with interactions lists greater than 0.
408     int atomsWithIteractions = 0;
409     asymmetricUnitCount = 0;
410     symmetryMateCount = 0;
411     // Collect interactions.
412     int[][] list = lists[0];
413     for (int i = 0; i < nAtoms; i++) {
414       asymmetricUnitCount += list[i].length;
415       if (listCount[i] > 0) {
416         atomsWithIteractions++;
417       }
418     }
419     for (int iSymm = 1; iSymm < nSymm; iSymm++) {
420       list = lists[iSymm];
421       for (int i = 0; i < nAtoms; i++) {
422         symmetryMateCount += list[i].length;
423       }
424     }
425 
426     groupAsymmetricUnitCount = 0;
427     groupSymmetryMateCount = 0;
428     if (M > 1) {
429       // Asymmetric unit group interactions.
430       list = groupLists[0];
431       for (int i = 0; i < numGroups; i++) {
432         groupAsymmetricUnitCount += list[i].length;
433       }
434       // Symmetry mate group interactions.
435       for (int iSymm = 1; iSymm < nSymm; iSymm++) {
436         list = groupLists[iSymm];
437         for (int i = 0; i < numGroups; i++) {
438           groupSymmetryMateCount += list[i].length;
439         }
440       }
441     }
442 
443     // Print the interaction counts.
444     if (print) {
445       print();
446     }
447 
448     // Update the pair-wise schedule.
449     long scheduleTime = -System.nanoTime();
450     pairwiseSchedule.updateRanges(sharedCount.get(), atomsWithIteractions, listCount);
451     scheduleTime += System.nanoTime();
452 
453     if (logger.isLoggable(Level.FINE)) {
454       time = System.nanoTime() - time;
455       StringBuilder sb = new StringBuilder();
456       sb.append(format("   Motion Check:           %6.4f sec\n", motionTime * 1e-9));
457       sb.append(format("   List Initialization:    %6.4f sec\n", initTime * 1e-9));
458       sb.append(format("   Assign Atoms to Cells:  %6.4f sec\n", assignAtomsToCellsTime * 1e-9));
459       sb.append(format("   Create Vertlet Lists:   %6.4f sec\n", verletListTime * 1e-9));
460       sb.append(format("   Parallel Schedule:      %6.4f sec\n", scheduleTime * 1e-9));
461       if (M >= 1) {
462         sb.append(format("   Group List Total:       %6.4f sec\n", groupingTime * 1e-9));
463       }
464       sb.append(format("   Neighbor List Total:    %6.4f sec\n", time * 1e-9));
465       logger.fine(sb.toString());
466     }
467   }
468 
469   /**
470    * Returns the cutoff distance used internally by NeighborList.
471    *
472    * @return Cutoff distance in Angstroms.
473    */
474   public double getCutoff() {
475     return cutoff;
476   }
477 
478   /**
479    * If disableUpdates true, disable updating the neighbor list upon motion. Use with caution; best
480    * recommendation is to only use if all atoms have a coordinate restraint.
481    *
482    * @param disableUpdate Disable updating the neighbor list
483    */
484   public void setDisableUpdates(boolean disableUpdate) {
485     this.disableUpdates = disableUpdate;
486   }
487 
488   /**
489    * Return the Verlet list.
490    *
491    * @return The Verlet list of size [nSymm][nAtoms][nNeighbors].
492    */
493   public int[][][] getNeighborList() {
494     return lists;
495   }
496 
497   /**
498    * Getter for the field <code>pairwiseSchedule</code>.
499    *
500    * @return a {@link ffx.potential.nonbonded.PairwiseSchedule} object.
501    */
502   public PairwiseSchedule getPairwiseSchedule() {
503     return pairwiseSchedule;
504   }
505 
506   /**
507    * {@inheritDoc}
508    *
509    * <p>This is method should not be called; it is invoked by Parallel Java.
510    *
511    * @since 1.0
512    */
513   @Override
514   public void run() {
515     if (disableUpdates) {
516       return;
517     }
518 
519     int threadIndex = getThreadIndex();
520     try {
521       if (!forceRebuild) {
522         // Check for motion.
523         if (threadIndex == 0) {
524           motionTime = -System.nanoTime();
525         }
526         execute(0, nAtoms - 1, motionLoops[threadIndex]);
527         if (threadIndex == 0) {
528           motionTime += System.nanoTime();
529         }
530       }
531       if (forceRebuild || sharedMotion.get()) {
532         // Complete some initializations.
533         barrier(listInitBarrierAction);
534         // Assign atoms to cells.
535         if (threadIndex == 0) {
536           assignAtomsToCellsTime = -System.nanoTime();
537         }
538         execute(0, nAtoms - 1, assignAtomsToCellsLoops[threadIndex]);
539         if (threadIndex == 0) {
540           assignAtomsToCellsTime += System.nanoTime();
541         }
542         // Build the Verlet list.
543         if (threadIndex == 0) {
544           verletListTime = -System.nanoTime();
545         }
546         execute(0, nAtoms - 1, verletListLoop[threadIndex]);
547         if (threadIndex == 0) {
548           verletListTime += System.nanoTime();
549         }
550         // Build Group Lists if desired
551         if (M > 1) {
552           if (threadIndex == 0) {
553             groupingTime = -System.nanoTime();
554             // Build groups of size M.
555             mGroups = buildGroups();
556             numGroups = mGroups.length;
557             mGroupID = assignGroupID(mGroups);
558             groupLists = new int[nSymm][numGroups][];
559           }
560           barrier();
561           execute(0, mGroups.length - 1, groupVerletListLoop[threadIndex]);
562           if (threadIndex == 0) {
563             groupingTime += System.nanoTime();
564           }
565         }
566       }
567     } catch (Exception e) {
568       String message =
569           "Fatal exception building neighbor list in thread: " + getThreadIndex() + "\n";
570       logger.log(Level.SEVERE, message, e);
571     }
572   }
573 
574   /**
575    * Build groups of size M by walking up Z columns. Must be called
576    * after the domain decomposition is updated.
577    *
578    * @return The groups of size M.
579    */
580   private int[][] buildGroups() {
581     // Use cells to build groups
582     int nA = domainDecomposition.nA;
583     int nB = domainDecomposition.nB;
584     int nC = domainDecomposition.nC;
585 
586     // Collect groups of size M from the asymmetric unit.
587     List<int[]> groupsList = new ArrayList<>();
588     int[] group = new int[M];
589     for (int i = 0; i < nA; i++) {
590       for (int j = 0; j < nB; j++) {
591         PriorityQueue<AtomIndex> queue = new PriorityQueue<>();
592         for (int k = 0; k < nC; k++) {
593           // Walk up z-columns adding to groups of size M
594           Cell cell = domainDecomposition.getCell(i, j, k);
595           for (AtomIndex atomIndex : cell.list) {
596             // Only consider atoms in the asymmetric unit.
597             if (atomIndex.iSymm != 0) {
598               continue;
599             }
600             queue.add(atomIndex);
601             // Flush queue into groups
602             while (queue.size() >= M) {
603               for (int m = 0; m < M; m++) {
604                 AtomIndex atom = queue.poll();
605                 group[m] = atom.i;
606               }
607               groupsList.add(group);
608               group = new int[M];
609             }
610           }
611         }
612         if (!queue.isEmpty()) {
613           // Fill in the final group for this column with flags (-1) to indicate empty slots.
614           fill(group, -1);
615           for (int l = 0; l < M; l++) {
616             if (!queue.isEmpty()) {
617               AtomIndex atom = queue.poll();
618               group[l] = atom.i;
619             }
620           }
621           groupsList.add(group);
622           group = new int[M];
623         }
624       }
625     }
626 
627     int numGroups = groupsList.size();
628     int[][] groups = new int[groupsList.size()][];
629     for (int i = 0; i < numGroups; i++) {
630       groups[i] = groupsList.get(i);
631       // logger.info(format(" Group %d: %s", i, Arrays.toString(groups[i])));
632     }
633     return groups;
634   }
635 
636   /**
637    * Assign group IDs to each atom in the groups.
638    *
639    * @param groups The groups of atoms.
640    * @return An array of group IDs for each atom.
641    */
642   private int[] assignGroupID(int[][] groups) {
643     int nGroups = groups.length;
644     int[] groupID = new int[nAtoms];
645     for (int i = 0; i < nGroups; i++) {
646       int[] group = groups[i];
647       for (int atomIndex : group) {
648         // Groups may contain -1 for empty slots, so check for valid index.
649         if (atomIndex >= 0) {
650           groupID[atomIndex] = i;
651         }
652       }
653     }
654     return groupID;
655   }
656 
657   /**
658    * The NeighborList will be re-configured, if necessary, for the supplied atom list.
659    *
660    * @param atoms A new list of atoms to operate on.
661    */
662   public void setAtoms(Atom[] atoms) {
663     this.atoms = atoms;
664     this.nAtoms = atoms.length;
665     initNeighborList(false);
666   }
667 
668   /**
669    * The NeighborList will be re-configured, if necessary, for the supplied Crystal. Changes to both
670    * unit cell parameters and number of symmetry operators are also acceptable.
671    *
672    * @param crystal A crystal defining boundary conditions and symmetry.
673    */
674   public void setCrystal(Crystal crystal) {
675     this.crystal = crystal;
676     initNeighborList(false);
677   }
678 
679   /**
680    * Setter for the field <code>intermolecular</code>.
681    *
682    * @param intermolecular If true, the NeighborList will include intermolecular interactions.
683    */
684   public void setIntermolecular(boolean intermolecular) {
685     this.intermolecular = intermolecular;
686   }
687 
688   /**
689    * {@inheritDoc}
690    *
691    * <p>This is method should not be called; it is invoked by Parallel Java.
692    *
693    * @since 1.0
694    */
695   @Override
696   public void start() {
697     if (disableUpdates) {
698       return;
699     }
700     time = System.nanoTime();
701     motionTime = 0;
702     initTime = 0;
703     assignAtomsToCellsTime = 0;
704     verletListTime = 0;
705     sharedMotion.set(false);
706   }
707 
708   private void initNeighborList(boolean print) {
709 
710     // Set the number of symmetry operators.
711     int newNSymm = crystal.spaceGroup.symOps.size();
712     if (nSymm != newNSymm) {
713       nSymm = newNSymm;
714     }
715 
716     // Allocate memory for fractional coordinates and subcell pointers for each atom.
717     if (previous == null || previous.length < 3 * nAtoms) {
718       previous = new double[3 * nAtoms];
719       listCount = new int[nAtoms];
720       pairwiseSchedule = new PairwiseSchedule(threadCount, nAtoms, ranges);
721     } else {
722       pairwiseSchedule.setAtoms(nAtoms);
723     }
724 
725     // Find the largest sphere that is enclosed by the unit cell.
726     final double sphere = min(min(crystal.interfacialRadiusA, crystal.interfacialRadiusB),
727         crystal.interfacialRadiusC);
728 
729     /*
730      Assert that the boundary conditions defined by the crystal allow use
731      of the minimum image condition.
732     */
733     if (!crystal.aperiodic()) {
734       assert (sphere >= cutoffPlusBuffer);
735     }
736 
737     domainDecomposition.initDomainDecomposition(nAtoms, crystal);
738     if (print) {
739       domainDecomposition.log();
740     }
741   }
742 
743   private void print() {
744     StringBuilder sb = new StringBuilder(
745         format("   Buffer:                                %5.2f (A)\n", buffer));
746     sb.append(format("   Cut-off:                               %5.2f (A)\n", cutoff));
747     sb.append(format("   Total:                                 %5.2f (A)\n", cutoffPlusBuffer));
748     sb.append(format("   Neighbors in the asymmetric unit:%11d\n", asymmetricUnitCount));
749     if (nSymm > 1) {
750       sb.append(format("   Neighbors in symmetry mates:    %12d\n", symmetryMateCount));
751       int num = (asymmetricUnitCount + symmetryMateCount) * nSymm;
752       sb.append(format("   Neighbors in the unit cell:     %12d\n", num));
753     }
754 
755     if (M > 1) {
756       sb.append(format("   Number of groups:                  %9d\n", numGroups));
757       sb.append(format("   Group pairs in the asymmetric unit:%9d\n", groupAsymmetricUnitCount));
758       if (nSymm > 1) {
759         sb.append(format("   Group pairs in symmetry mates:  %12d\n", groupSymmetryMateCount));
760         int num = (groupAsymmetricUnitCount + groupSymmetryMateCount) * nSymm;
761         sb.append(format("   Group pairs in the unit cell:   %12d\n", num));
762       }
763     }
764 
765     logger.info(sb.toString());
766   }
767 
768   private static final int XX = 0;
769   private static final int YY = 1;
770   private static final int ZZ = 2;
771 
772   /**
773    * Detect if any atom has moved 1/2 the buffer size.
774    *
775    * @since 1.0
776    */
777   private class MotionLoop extends IntegerForLoop {
778 
779     @Override
780     public void run(int lb, int ub) {
781       double[] current = coordinates[0];
782       for (int i = lb; i <= ub; i++) {
783         int i3 = i * 3;
784         int iX = i3 + XX;
785         int iY = i3 + YY;
786         int iZ = i3 + ZZ;
787         double dx = previous[iX] - current[iX];
788         double dy = previous[iY] - current[iY];
789         double dz = previous[iZ] - current[iZ];
790         double dr2 = crystal.image(dx, dy, dz);
791         if (dr2 > motion2) {
792           if (logger.isLoggable(Level.FINE)) {
793             logger.fine(format(" Motion detected for atom %d (%8.6f A).", i, sqrt(dr2)));
794           }
795           sharedMotion.set(true);
796         }
797       }
798     }
799   }
800 
801   /**
802    * Perform some initialization tasks prior to list rebuild.
803    */
804   private class ListInitBarrierAction extends BarrierAction {
805 
806     @Override
807     public void run() {
808       initTime = -System.nanoTime();
809       // Clear the list count.
810       sharedCount.set(0);
811 
812       domainDecomposition.clear();
813 
814       // Allocate memory for neighbor lists.
815       for (int iSymm = 0; iSymm < nSymm; iSymm++) {
816         if (lists[iSymm] == null || lists[iSymm].length < nAtoms) {
817           lists[iSymm] = new int[nAtoms][];
818         }
819       }
820       initTime += System.nanoTime();
821     }
822   }
823 
824   /**
825    * Assign atoms to cells.
826    *
827    * @since 1.0
828    */
829   private class AssignAtomsToCellsLoop extends IntegerForLoop {
830 
831     /**
832      * The cartesian coordinates of the atom in the asymmetric unit.
833      */
834     private final double[] auCart = new double[3];
835     /**
836      * The cartesian coordinates of the atom after symmetry application.
837      */
838     private final double[] cart = new double[3];
839     /**
840      * The fractional coordinates of the atom after symmetry application.
841      */
842     private final double[] frac = new double[3];
843 
844     @Override
845     public void run(int lb, int ub) {
846 
847       // Save the current coordinates.
848       double[] current = coordinates[0];
849       for (int i = lb; i <= ub; i++) {
850         int i3 = i * 3;
851         int iX = i3 + XX;
852         int iY = i3 + YY;
853         int iZ = i3 + ZZ;
854         previous[iX] = current[iX];
855         previous[iY] = current[iY];
856         previous[iZ] = current[iZ];
857       }
858 
859       final double[] auXYZ = coordinates[0];
860       final double spCutoff2 = crystal.getSpecialPositionCutoff2();
861       for (int iSymm = 0; iSymm < nSymm; iSymm++) {
862         final double[] xyz = coordinates[iSymm];
863         // Assign each atom to a cell using fractional coordinates.
864         for (int i = lb; i <= ub; i++) {
865           int i3 = i * 3;
866           cart[0] = xyz[i3 + XX];
867           cart[1] = xyz[i3 + YY];
868           cart[2] = xyz[i3 + ZZ];
869           if (iSymm != 0) {
870             auCart[0] = auXYZ[i3 + XX];
871             auCart[1] = auXYZ[i3 + YY];
872             auCart[2] = auXYZ[i3 + ZZ];
873             double dx = auCart[0] - cart[0];
874             double dy = auCart[1] - cart[1];
875             double dz = auCart[2] - cart[2];
876             double dr2 = crystal.image(dx, dy, dz);
877             if (dr2 < spCutoff2) {
878               int molecule = atoms[i].getMoleculeNumber();
879               if (atoms[i].isActive()) {
880                 logger.info(format("   Active Atom %s at Special Position in Molecule %d with SymOp %d (%8.6f A).",
881                     atoms[i], molecule, iSymm, sqrt(dr2)));
882               } else if (logger.isLoggable(Level.FINE)) {
883                 logger.fine(format("   Atom %s at Special Position in Molecule %d with SymOp %d (%8.6f A).",
884                     atoms[i], molecule, iSymm, sqrt(dr2)));
885               }
886               // Exclude molecule-molecule interactions between the asymmetric unit and the iSymm symmetry mate.
887               domainDecomposition.addSpecialPositionExclusion(molecule, iSymm);
888             }
889           }
890           // Convert to fractional coordinates.
891           crystal.toFractionalCoordinates(cart, frac);
892           domainDecomposition.addAtomToCell(i, iSymm, frac);
893         }
894       }
895     }
896   }
897 
898   /**
899    * The VerletListLoop class encapsulates thread local variables and methods for building Verlet
900    * lists based on a spatial decomposition of the unit cell.
901    *
902    * @author Michael J. Schnieders
903    * @since 1.0
904    */
905   private class VerletListLoop extends IntegerForLoop {
906 
907     private final IntegerSchedule schedule;
908     private int n;
909     private int iSymm;
910     private int atomIndex;
911     private boolean iactive = true;
912     private int count;
913     private double[] xyz;
914     private int[] pairs;
915     private Cell cellForCurrentAtom;
916     private int[] pairCellAtoms;
917 
918     VerletListLoop() {
919       int len = 1000;
920       pairs = new int[len];
921       schedule = IntegerSchedule.dynamic(10);
922       pairCellAtoms = new int[len];
923     }
924 
925     @Override
926     public void finish() {
927       sharedCount.addAndGet(count);
928     }
929 
930     @Override
931     public void run(final int lb, final int ub) {
932       int nEdge = domainDecomposition.nEdge;
933       int nA = domainDecomposition.nA;
934       int nB = domainDecomposition.nB;
935       int nC = domainDecomposition.nC;
936       for (iSymm = 0; iSymm < nSymm; iSymm++) {
937         int[][] list = lists[iSymm];
938         // Loop over all atoms.
939         for (atomIndex = lb; atomIndex <= ub; atomIndex++) {
940           n = 0;
941 
942           if (iSymm == 0) {
943             listCount[atomIndex] = 0;
944             int molecule = atoms[atomIndex].getMoleculeNumber();
945             List<Integer> symOpList = domainDecomposition.getSpecialPositionSymOps(molecule);
946             atoms[atomIndex].setSpecialPositionSymOps(symOpList);
947           }
948 
949           if (use == null || use[atomIndex]) {
950 
951             iactive = atoms[atomIndex].isActive();
952 
953             cellForCurrentAtom = domainDecomposition.getCellForAtom(atomIndex);
954             int a = cellForCurrentAtom.a;
955             int b = cellForCurrentAtom.b;
956             int c = cellForCurrentAtom.c;
957             int a1 = a + 1;
958             int b1 = b + 1;
959             int c1 = c + 1;
960 
961             /*
962              If the number of divisions is 1 in any direction, then
963              set the loop limits to the current cell value.
964              Otherwise, search from a - nEdge to a + nEdge.
965             */
966             int aStart = nA == 1 ? a : a - nEdge;
967             int aStop = nA == 1 ? a : a + nEdge;
968             int bStart = nB == 1 ? b : b - nEdge;
969             int bStop = nB == 1 ? b : b + nEdge;
970             int cStart = nC == 1 ? c : c - nEdge;
971             int cStop = nC == 1 ? c : c + nEdge;
972 
973             if (iSymm == 0) {
974               // Interactions within the "self-volume".
975               atomCellPairs(cellForCurrentAtom);
976               // Search half of the neighboring volumes to avoid double counting.
977               // (a, b+1..b+nE, c) -> radial line in y-dir
978               for (int bi = b1; bi <= bStop; bi++) {
979                 atomCellPairs(domainDecomposition.image(a, bi, c));
980               }
981               // (a, b-nE..b+nE, c+1..c+nE) -> cube in in z,y plane
982               for (int bi = bStart; bi <= bStop; bi++) {
983                 for (int ci = c1; ci <= cStop; ci++) {
984                   atomCellPairs(domainDecomposition.image(a, bi, ci));
985                 }
986               }
987               // (a+1..a+nE, b-nE..b+nE, c-nE..c+nE) -> cube pushed in x-dir
988               for (int bi = bStart; bi <= bStop; bi++) {
989                 for (int ci = cStart; ci <= cStop; ci++) {
990                   for (int ai = a1; ai <= aStop; ai++) {
991                     atomCellPairs(domainDecomposition.image(ai, bi, ci));
992                   }
993                 }
994               }
995             } else {
996               // Interactions with all adjacent symmetry mate cells.
997               for (int ai = aStart; ai <= aStop; ai++) {
998                 for (int bi = bStart; bi <= bStop; bi++) {
999                   for (int ci = cStart; ci <= cStop; ci++) {
1000                     atomCellPairs(domainDecomposition.image(ai, bi, ci));
1001                   }
1002                 }
1003               }
1004             }
1005           }
1006 
1007           list[atomIndex] = new int[n];
1008           listCount[atomIndex] += n;
1009           count += n;
1010           arraycopy(pairs, 0, list[atomIndex], 0, n);
1011         }
1012       }
1013     }
1014 
1015     @Override
1016     public IntegerSchedule schedule() {
1017       return schedule;
1018     }
1019 
1020     @Override
1021     public void start() {
1022       xyz = coordinates[0];
1023       count = 0;
1024     }
1025 
1026     private void atomCellPairs(final Cell cell) {
1027       if (pairCellAtoms.length < cell.getCount()) {
1028         pairCellAtoms = new int[cell.getCount()];
1029       }
1030 
1031       // Load the cell's atom indices into an array for the specified SymOp.
1032       int num = cell.getSymOpAtoms(iSymm, pairCellAtoms);
1033       if (num == 0) {
1034         return;
1035       }
1036 
1037       final int moleculeIndex = atoms[atomIndex].getMoleculeNumber();
1038 
1039       boolean logClosePairs = logger.isLoggable(Level.FINE);
1040 
1041       final int i3 = atomIndex * 3;
1042       final double xi = xyz[i3 + XX];
1043       final double yi = xyz[i3 + YY];
1044       final double zi = xyz[i3 + ZZ];
1045       final double[] pair = coordinates[iSymm];
1046 
1047       // Loop over atoms in the "pair" cell.
1048       for (int j = 0; j < num; j++) {
1049         final int aj = pairCellAtoms[j];
1050 
1051         // If the self-volume is being searched for pairs, avoid double counting.
1052         if (iSymm == 0 && cell == cellForCurrentAtom) {
1053           // Only consider atoms with a higher index than the current atom.
1054           if (aj <= atomIndex) {
1055             continue;
1056           }
1057         }
1058 
1059         if (use != null && !use[aj]) {
1060           continue;
1061         }
1062 
1063         if (!includeInactivePairs && !iactive) {
1064           if (!atoms[aj].isActive()) {
1065             continue;
1066           }
1067         }
1068 
1069         int moleculeIndexJ = atoms[aj].getMoleculeNumber();
1070         // Check if this pair search is only intra-molecular.
1071         if (!intermolecular && (moleculeIndex != moleculeIndexJ)) {
1072           continue;
1073         }
1074 
1075         // Check for a special position exclusion between two atoms in the same molecule.
1076         if (iSymm != 0 && (moleculeIndex == moleculeIndexJ)) {
1077           if (domainDecomposition.isSpecialPositionExclusion(moleculeIndex, iSymm)) {
1078             if (logger.isLoggable(Level.FINEST)) {
1079               logger.finest(
1080                   format("   Excluding Interaction for Atoms %d and %d in Molecule %d for SymOp %d.",
1081                       atomIndex, aj, moleculeIndex, iSymm));
1082             }
1083             continue;
1084           }
1085         }
1086 
1087         if (iSymm == 0 || aj >= atomIndex) {
1088           int aj3 = aj * 3;
1089           final double xj = pair[aj3 + XX];
1090           final double yj = pair[aj3 + YY];
1091           final double zj = pair[aj3 + ZZ];
1092           final double xr = xi - xj;
1093           final double yr = yi - yj;
1094           final double zr = zi - zj;
1095           final double d2 = crystal.image(xr, yr, zr);
1096           if (d2 <= cutoffPlusBuffer2) {
1097             if (logClosePairs && d2 < crystal.getSpecialPositionCutoff2()) {
1098               // Warn about close interactions.
1099               logger.fine(
1100                   format(" Close interaction (%6.3f) between atoms (iSymm = %d):\n %s\n %s\n",
1101                       sqrt(d2), iSymm, atoms[atomIndex].toString(), atoms[aj].toString()));
1102             }
1103 
1104             // Add the pair to the list, reallocating the array size if necessary.
1105             try {
1106               pairs[n++] = aj;
1107             } catch (Exception e) {
1108               n = pairs.length;
1109               pairs = copyOf(pairs, n + 100);
1110               pairs[n++] = aj;
1111             }
1112           }
1113         }
1114       }
1115     }
1116 
1117   }
1118 
1119   /**
1120    * Condense the atom-based Verlet list into a group-based Verlet list.
1121    */
1122   private class GroupVerletListLoop extends IntegerForLoop {
1123 
1124     private final IntegerSchedule schedule = IntegerSchedule.dynamic(10);
1125 
1126     @Override
1127     public IntegerSchedule schedule() {
1128       return schedule;
1129     }
1130 
1131     @Override
1132     public void run(final int lb, final int ub) {
1133       // Build the Verlet Group Lists
1134       List<Integer> neighborGroups = new ArrayList<>();
1135       for (int iSymm = 0; iSymm < nSymm; iSymm++) {
1136         // Loop over M groups.
1137         for (int groupIndex = lb; groupIndex <= ub; groupIndex++) {
1138           // Loop over atoms from the M group.
1139           for (int a = 0; a < M; a++) {
1140             int atomIndex = mGroups[groupIndex][a];
1141             // Some groups may have -1 for empty slots.
1142             if (atomIndex == -1) {
1143               continue;
1144             }
1145             // Loop over neighbors of the atom.
1146             int[] neighborList = lists[iSymm][atomIndex];
1147             for (int value : neighborList) {
1148               // The M-Group ID of the neighbor atom.
1149               int neighborGroupIndex = mGroupID[value];
1150               if (!neighborGroups.contains(neighborGroupIndex)) {
1151                 if (groupIndex <= neighborGroupIndex) {
1152                   // If the neighbor group index is greater than or equal to the current group index,
1153                   // then include it in the neighbor list.
1154                   neighborGroups.add(neighborGroupIndex);
1155                 } else if (neighborGroupIndex >= lb) {
1156                   // This thread has already processed the lower index group.
1157                   // Check if the group with the lower index already includes the current group in its list.
1158                   int[] list = groupLists[iSymm][neighborGroupIndex];
1159                   boolean included = false;
1160                   for (int neighbor : list) {
1161                     // Check if the group with a lower ID already includes the higher ID in its list.
1162                     if (neighbor == groupIndex) {
1163                       included = true;
1164                       break;
1165                     }
1166                   }
1167                   if (!included) {
1168                     // If the group with a lower ID does not include the higher ID, so add it.
1169                     neighborGroups.add(neighborGroupIndex);
1170                   }
1171                 } else if (!isIncludedInLowerIDList(iSymm, neighborGroupIndex, groupIndex)) {
1172                   // Otherwise, check if the group with the lower index will include the current group in its list.
1173                   neighborGroups.add(neighborGroupIndex);
1174                 }
1175               }
1176             }
1177           }
1178 
1179           // Set the list of neighboring groups.
1180           // logger.info(" SymOp " + iSymm + " Group " + groupIndex + " has " + neighborGroups.size() + " neighbors.");
1181           groupLists[iSymm][groupIndex] = neighborGroups.stream().mapToInt(Integer::intValue).toArray();
1182           // Clear the list for the next M group.
1183           neighborGroups.clear();
1184         }
1185       }
1186     }
1187   }
1188 
1189   /**
1190    * Check if the group will smaller index includes 2nd group in its group neighbor list.
1191    *
1192    * @param symOp             The symmetry operator index.
1193    * @param smallerGroupIndex The group with the smaller index.
1194    * @param secondGroup       The second group with the larger index.
1195    * @return True if the higher ID group should include the lower ID group in its list.
1196    */
1197   private boolean isIncludedInLowerIDList(int symOp, int smallerGroupIndex, int secondGroup) {
1198     if (smallerGroupIndex >= secondGroup) {
1199       // The lower ID group is not less than the higher ID group.
1200       return false;
1201     }
1202     // Loop over atoms from the lower ID Group.
1203     for (int a = 0; a < M; a++) {
1204       int atomID = mGroups[smallerGroupIndex][a];
1205       // Some groups may have -1 for empty slots.
1206       if (atomID == -1) {
1207         continue;
1208       }
1209       // Loop over neighbors of the atom.
1210       int[] neighborList = lists[symOp][atomID];
1211       for (int value : neighborList) {
1212         // Get the M-Group ID of the neighbor atom.
1213         int neighborGroupID = mGroupID[value];
1214         if (neighborGroupID == secondGroup) {
1215           // The lower ID group will include the higher ID group in its neighbor list.
1216           return true;
1217         }
1218       }
1219     }
1220     return false;
1221   }
1222 
1223   /**
1224    * Break down the simulation domain into a 3D grid of cells.
1225    */
1226   private static class DomainDecomposition {
1227 
1228     /**
1229      * The crystal that defines the simulation domain.
1230      */
1231     private Crystal crystal;
1232     /**
1233      * The targetInterfacialRadius is the smallest interfacial radius for a subcell, given a search
1234      * of nEdge subcells along an axis, that assures all neighbors will be found.
1235      */
1236     private final double targetInterfacialRadius;
1237     /**
1238      * The number of subcells that must be searched along an axis to find all neighbors within
1239      * the cutoff + buffer distance.
1240      *
1241      * <p>If the nEdge == 1 for {X=A,B,C} then all neighbors will be found in 3x3x3 = 27 cells.
1242      * If each nEdge == 2, then all neighbors will be found in 5x5x5 = 125 cells (in this case the
1243      * cells are smaller).
1244      */
1245     private final int nEdge;
1246     /**
1247      * The number of subcells that must be searched along an axis to find all neighbors within the
1248      * cutoff + buffer distance. nSearch = 2 * nEdge + 1
1249      */
1250     private final int nSearch;
1251     /**
1252      * The number of divisions along the A-axis.
1253      */
1254     private int nA;
1255     /**
1256      * The number of divisions along the B-axis.
1257      */
1258     private int nB;
1259     /**
1260      * The number of divisions along the C-Axis.
1261      */
1262     private int nC;
1263     /**
1264      * The number of atoms.
1265      */
1266     private int nAtoms;
1267     /**
1268      * The cell indices of each atom along the A-axis.
1269      */
1270     private int[] cellA;
1271     /**
1272      * The cell indices of each atom along the B-axis.
1273      */
1274     private int[] cellB;
1275     /**
1276      * The cell indices of each atom along the C-axis.
1277      */
1278     private int[] cellC;
1279     /**
1280      * The fractional sub-volumes of the unit cell.
1281      */
1282     private Cell[][][] cells;
1283     /**
1284      * Special position exclusions. Map<Molecule, List<SymOpIndex>>
1285      */
1286     private Map<Integer, List<Integer>> specialPositionExclusionMap;
1287 
1288 
1289     /**
1290      * DomainDecomposition Constructor.
1291      *
1292      * @param nAtoms           Number of atoms.
1293      * @param crystal          The crystal.
1294      * @param cutoffPlusBuffer The cutoff plus buffer distance.
1295      */
1296     public DomainDecomposition(int nAtoms, Crystal crystal, double cutoffPlusBuffer) {
1297       this.nEdge = 2;
1298       this.nSearch = 2 * nEdge + 1;
1299       targetInterfacialRadius = cutoffPlusBuffer / (double) nEdge;
1300       initDomainDecomposition(nAtoms, crystal);
1301     }
1302 
1303     /**
1304      * Initialize the domain decomposition.
1305      */
1306     public void initDomainDecomposition(int nAtoms, Crystal crystal) {
1307       this.nAtoms = nAtoms;
1308       this.crystal = crystal;
1309 
1310       // Allocate memory for fractional coordinates and subcell pointers for each atom.
1311       if (cellA == null || cellA.length < nAtoms) {
1312         cellA = new int[nAtoms];
1313         cellB = new int[nAtoms];
1314         cellC = new int[nAtoms];
1315       }
1316 
1317       // Determine the number of subcells along each axis.
1318       int maxA = (int) floor(2 * crystal.interfacialRadiusA / targetInterfacialRadius);
1319       int maxB = (int) floor(2 * crystal.interfacialRadiusB / targetInterfacialRadius);
1320       int maxC = (int) floor(2 * crystal.interfacialRadiusC / targetInterfacialRadius);
1321 
1322       /*
1323         To satisfy the cutoff + buffer distance, the number of subcells (maxA, maxB, maxC)
1324         is guaranteed to be at least nSearch - 1 for periodic crystals.
1325         */
1326       if (!crystal.aperiodic()) {
1327         assert (maxA >= nSearch - 1);
1328         assert (maxB >= nSearch - 1);
1329         assert (maxC >= nSearch - 1);
1330       }
1331 
1332       /*
1333         If the number of subcells less than nSearch, then turn off the overhead of the
1334         subcell search by setting the number of cells along that axis to 1.
1335        */
1336       nA = maxA < nSearch ? 1 : maxA;
1337       nB = maxB < nSearch ? 1 : maxB;
1338       nC = maxC < nSearch ? 1 : maxC;
1339 
1340       // Allocate memory for the subcells.
1341       if (cells == null || cells.length != nA || cells[0].length != nB || cells[0][0].length != nC) {
1342         cells = new Cell[nA][nB][nC];
1343         for (int i = 0; i < nA; i++) {
1344           for (int j = 0; j < nB; j++) {
1345             for (int k = 0; k < nC; k++) {
1346               cells[i][j][k] = new Cell(i, j, k);
1347             }
1348           }
1349         }
1350       } else {
1351         clear();
1352       }
1353 
1354     }
1355 
1356     /**
1357      * Clear the sub-cells of the atom list.
1358      */
1359     public void clear() {
1360       // Clear cell contents.
1361       for (int i = 0; i < nA; i++) {
1362         for (int j = 0; j < nB; j++) {
1363           for (int k = 0; k < nC; k++) {
1364             cells[i][j][k].clear();
1365           }
1366         }
1367       }
1368 
1369       // Clear special position exclusions.
1370       // if (specialPositionExclusionMap != null) {
1371       // Clear the lists, but don't remove the keys.
1372       //   for (List<Integer> list : specialPositionExclusionMap.values()) {
1373       //     list.clear();
1374       //   }
1375       //   specialPositionExclusionMap.clear();
1376       // }
1377     }
1378 
1379     /**
1380      * Add a special position exclusion.
1381      *
1382      * @param molecule   The molecule.
1383      * @param symOpIndex The symmetry operation index.
1384      */
1385     public void addSpecialPositionExclusion(int molecule, int symOpIndex) {
1386       // Initialize the map.
1387       if (specialPositionExclusionMap == null) {
1388         specialPositionExclusionMap = new HashMap<>();
1389       }
1390       // Initialize the list for the molecule.
1391       List<Integer> list = specialPositionExclusionMap.get(molecule);
1392       if (list == null) {
1393         list = new ArrayList<>();
1394         specialPositionExclusionMap.put(molecule, list);
1395       }
1396       // Add the symOpIndex to the list.
1397       if (!list.contains(symOpIndex)) {
1398         list.add(symOpIndex);
1399       }
1400     }
1401 
1402     /**
1403      * Check if a special position exclusion exists.
1404      *
1405      * @param molecule   The molecule.
1406      * @param symOpIndex The symmetry operation index.
1407      * @return True if the special position exclusion exists.
1408      */
1409     public boolean isSpecialPositionExclusion(int molecule, int symOpIndex) {
1410       if (specialPositionExclusionMap == null) {
1411         return false;
1412       }
1413       List<Integer> list = specialPositionExclusionMap.get(molecule);
1414       if (list == null) {
1415         return false;
1416       }
1417       return list.contains(symOpIndex);
1418     }
1419 
1420     /**
1421      * Get the special position SymOps for a molecule.
1422      *
1423      * @param molecule The molecule.
1424      * @return The special position SymOps.
1425      */
1426     public List<Integer> getSpecialPositionSymOps(int molecule) {
1427       if (specialPositionExclusionMap == null) {
1428         return null;
1429       }
1430       if (specialPositionExclusionMap.containsKey(molecule)) {
1431         List<Integer> list = specialPositionExclusionMap.get(molecule);
1432         if (list.isEmpty()) {
1433           return null;
1434         } else {
1435           return list;
1436         }
1437       }
1438       return null;
1439     }
1440 
1441     /**
1442      * Log information about the domain decomposition.
1443      */
1444     public void log() {
1445       int nCells = nA * nB * nC;
1446       int nSymm = crystal.spaceGroup.getNumberOfSymOps();
1447       StringBuilder sb = new StringBuilder("  Neighbor List Builder\n");
1448       sb.append(format("   Total Cells:          %8d\n", nCells));
1449       if (nCells > 1) {
1450         sb.append(format("   Interfacial radius    %8.3f A\n", targetInterfacialRadius));
1451         int searchA = nA == 1 ? 1 : nSearch;
1452         int searchB = nB == 1 ? 1 : nSearch;
1453         int searchC = nC == 1 ? 1 : nSearch;
1454         sb.append(format("   Domain Decomposition: %8d %4d %4d\n", nA, nB, nC));
1455         sb.append(format("   Neighbor Search:      %8d x%3d x%3d\n", searchA, searchB, searchC));
1456         sb.append(format("   Mean Atoms per Cell:  %8d", nAtoms * nSymm / nCells));
1457       }
1458       logger.info(sb.toString());
1459     }
1460 
1461     /**
1462      * If the index is >= to nX, it is mapped back into the periodic unit cell by subtracting nX. If
1463      * the index is less than 0, it is mapped into the periodic unit cell by adding nX. The Neighbor
1464      * list algorithm never requires multiple additions or subtractions of nX.
1465      *
1466      * @param i The index along the a-axis.
1467      * @param j The index along the b-axis.
1468      * @param k The index along the c-axis.
1469      * @return The requested Cell.
1470      */
1471     public Cell image(int i, int j, int k) {
1472       if (i >= nA) {
1473         i -= nA;
1474       } else if (i < 0) {
1475         i += nA;
1476       }
1477       if (j >= nB) {
1478         j -= nB;
1479       } else if (j < 0) {
1480         j += nB;
1481       }
1482       if (k >= nC) {
1483         k -= nC;
1484       } else if (k < 0) {
1485         k += nC;
1486       }
1487       return cells[i][j][k];
1488     }
1489 
1490     /**
1491      * Add an atom to a sub-cell.
1492      *
1493      * @param i     The index of the atom.
1494      * @param iSymm The index of the symmetry operator.
1495      * @param frac  The fractional coordinates of the atom.
1496      */
1497     public void addAtomToCell(int i, int iSymm, double[] frac) {
1498       double xu = frac[0];
1499       double yu = frac[1];
1500       double zu = frac[2];
1501       // Move the atom into the range 0.0 <= x < 1.0
1502       while (xu < 0.0) {
1503         xu += 1.0;
1504       }
1505       while (xu >= 1.0) {
1506         xu -= 1.0;
1507       }
1508       while (yu < 0.0) {
1509         yu += 1.0;
1510       }
1511       while (yu >= 1.0) {
1512         yu -= 1.0;
1513       }
1514       while (zu < 0.0) {
1515         zu += 1.0;
1516       }
1517       while (zu >= 1.0) {
1518         zu -= 1.0;
1519       }
1520       // The cell indices of this atom.
1521       final int a = (int) floor(xu * nA);
1522       final int b = (int) floor(yu * nB);
1523       final int c = (int) floor(zu * nC);
1524 
1525       if (iSymm == 0) {
1526         // Set the sub-cell indices for an asymmetric unit atom.
1527         cellA[i] = a;
1528         cellB[i] = b;
1529         cellC[i] = c;
1530       }
1531       cells[a][b][c].add(i, iSymm, zu);
1532     }
1533 
1534     /**
1535      * Get the sub-cell for an asymmetric unit atom.
1536      *
1537      * @param i The index of the atom.
1538      * @return The sub-cell for the atom.
1539      */
1540     public Cell getCellForAtom(int i) {
1541       return cells[cellA[i]][cellB[i]][cellC[i]];
1542     }
1543 
1544     public Cell getCell(int a, int b, int c) {
1545       if (a < 0 || a >= nA || b < 0 || b >= nB || c < 0 || c >= nC) {
1546         return null;
1547       }
1548       return cells[a][b][c];
1549     }
1550   }
1551 
1552   /**
1553    * Hold the atom index and its symmetry operator.
1554    */
1555   public static class AtomIndex implements Comparable<AtomIndex> {
1556 
1557     public final int iSymm;
1558     public final int i;
1559     public final double z;
1560 
1561     public AtomIndex(int iSymm, int i, double z) {
1562       this.iSymm = iSymm;
1563       this.i = i;
1564       this.z = z;
1565     }
1566 
1567     @Override
1568     public int compareTo(AtomIndex o) {
1569       return Double.compare(this.z, o.z);
1570     }
1571   }
1572 
1573   /**
1574    * Hold the atoms in each cell.
1575    */
1576   public static class Cell {
1577 
1578     /**
1579      * The cell index along the A-axis.
1580      */
1581     final int a;
1582     /**
1583      * The cell index along the B-axis.
1584      */
1585     final int b;
1586     /**
1587      * The cell index along the C-axis.
1588      */
1589     final int c;
1590 
1591     /**
1592      * The list of atoms in the cell, together with their symmetry operator.
1593      */
1594     final List<AtomIndex> list;
1595     /**
1596      * List of groupIDs that contain at least one atom in this cell.
1597      */
1598     final Set<Integer> groupList;
1599 
1600     public Cell(int a, int b, int c) {
1601       this.a = a;
1602       this.b = b;
1603       this.c = c;
1604       list = Collections.synchronizedList(new ArrayList<>());
1605       groupList = Collections.synchronizedSet(new HashSet<>());
1606     }
1607 
1608     /**
1609      * Add an atom to the cell.
1610      *
1611      * @param atomIndex  The atom index.
1612      * @param symOpIndex The symmetry operator index.
1613      */
1614     public void add(int atomIndex, int symOpIndex, double z) {
1615       list.add(new AtomIndex(symOpIndex, atomIndex, z));
1616     }
1617 
1618     public void groupAdd(int groupIndex) {
1619       groupList.add(groupIndex);
1620     }
1621 
1622     public AtomIndex get(int index) {
1623       if (index >= list.size()) {
1624         return null;
1625       }
1626       return list.get(index);
1627     }
1628 
1629     public int getCount() {
1630       return list.size();
1631     }
1632 
1633     public int getGroupCount() {
1634       return groupList.size();
1635     }
1636 
1637     /**
1638      * Return the number of atoms in the cell for a given symmetry operator.
1639      *
1640      * @param symOpIndex The symmetry operator index.
1641      * @param index      The list of indexes for the given symmetry operator.
1642      * @return The number of atoms in the cell for the symmetry operator.
1643      */
1644     public int getSymOpAtoms(int symOpIndex, int[] index) {
1645       int count = 0;
1646       for (AtomIndex atomIndex : list) {
1647         if (atomIndex.iSymm == symOpIndex) {
1648           if (count >= index.length) {
1649             throw new IndexOutOfBoundsException(
1650                 "Index out of bounds: count " + count + " " + index.length + " " + list.size());
1651           }
1652           index[count++] = atomIndex.i;
1653         }
1654       }
1655       return count;
1656     }
1657 
1658     /**
1659      * Clear the list of atoms in the cell.
1660      */
1661     public void clear() {
1662       list.clear();
1663     }
1664 
1665   }
1666 
1667   /**
1668    * Debugging method.
1669    *
1670    * @param args Command line arguments.
1671    */
1672   public static void main(String[] args) {
1673     PotentialsUtils potentialsUtils = new PotentialsUtils();
1674     File xyzFile = new File("./examples/waterbox.xyz");
1675     if (!xyzFile.exists()) {
1676       System.out.println("File does not exist");
1677     }
1678     MolecularAssembly molecularAssembly = potentialsUtils.open(xyzFile);
1679     molecularAssembly.getPotentialEnergy().energy(null, true);
1680   }
1681 }