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.algorithms.optimize.manybody;
39  
40  import edu.rit.pj.ParallelTeam;
41  import ffx.algorithms.AlgorithmListener;
42  import ffx.algorithms.optimize.RotamerOptimization;
43  import ffx.crystal.Crystal;
44  import ffx.crystal.SymOp;
45  import ffx.potential.MolecularAssembly;
46  import ffx.potential.bonded.Atom;
47  import ffx.potential.bonded.MultiResidue;
48  import ffx.potential.bonded.Residue;
49  import ffx.potential.bonded.ResidueState;
50  import ffx.potential.bonded.Rotamer;
51  import ffx.potential.bonded.RotamerLibrary;
52  import ffx.potential.nonbonded.NeighborList;
53  import org.apache.commons.math3.util.CombinatoricsUtils;
54  import org.apache.commons.math3.util.FastMath;
55  
56  import java.util.Arrays;
57  import java.util.HashMap;
58  import java.util.List;
59  import java.util.Map;
60  import java.util.logging.Level;
61  import java.util.logging.Logger;
62  
63  import static java.lang.String.format;
64  import static org.apache.commons.math3.util.FastMath.min;
65  import static org.apache.commons.math3.util.FastMath.sqrt;
66  
67  /**
68   * Calculates a residue-residue distance matrix.
69   *
70   * <p>Residue-residue distance is defined as the shortest atom-atom distance in any possible
71   * rotamer-rotamer pair if the residues are neighbors (central atom-central atom distances are within
72   * a cutoff). Otherwise, distances are set to a default of Double.MAX_VALUE.
73   *
74   * <p>The intent of using a neighbor list is to avoid tediously searching rotamer- rotamer pairs
75   * when two residues are so far apart we will never need the exact distance. We use the distance
76   * matrix for adding residues to the sliding window and determining whether to set 3-body energy to
77   * 0.0.
78   *
79   * <p>If the central atoms are too distant from each other, we can safely assume no atoms will ever
80   * be close enough for addition to sliding window or to cause explicit calculation of 3-body energy.
81   */
82  public class DistanceMatrix {
83  
84    private static final Logger logger = Logger.getLogger(DistanceMatrix.class.getName());
85  
86    /**
87     * MolecularAssembly to perform rotamer optimization on.
88     */
89    private final MolecularAssembly molecularAssembly;
90    /**
91     * AlgorithmListener who should receive updates as the optimization runs.
92     */
93    private final AlgorithmListener algorithmListener;
94    /**
95     * An array of all residues being optimized. Note that Box and Window optimizations operate on
96     * subsets of this list.
97     */
98    private final Residue[] allResiduesArray;
99    /**
100    * A list of all residues being optimized. Note that Box and Window optimizations operate on
101    * subsets of this list.
102    */
103   private final List<Residue> allResiduesList;
104   /**
105    * Number of residues being optimized.
106    */
107   private final int numResidues;
108   /**
109    * Default distance method is to find the shortest distance between residues.
110    */
111   private final RotamerOptimization.DistanceMethod distanceMethod;
112   /**
113    * The distance that the distance matrix checks for.
114    */
115   private final double distance;
116   /**
117    * Two-Body cutoff distance.
118    */
119   private final double twoBodyCutoffDist;
120   /**
121    * Three-body cutoff distance.
122    */
123   private final double threeBodyCutoffDist;
124   /**
125    * The minimum distance between atoms of a residue pair, taking into account interactions with
126    * symmetry mates.
127    */
128   private NeighborDistances[][] distanceMatrix;
129 
130   public DistanceMatrix(MolecularAssembly molecularAssembly, AlgorithmListener algorithmListener,
131                         Residue[] allResiduesArray, List<Residue> allResiduesList,
132                         RotamerOptimization.DistanceMethod distanceMethod, double distance, double twoBodyCutoffDist,
133                         double threeBodyCutoffDist) {
134     this.molecularAssembly = molecularAssembly;
135     this.algorithmListener = algorithmListener;
136     this.allResiduesArray = allResiduesArray;
137     this.allResiduesList = allResiduesList;
138     this.numResidues = allResiduesArray.length;
139     this.distanceMethod = distanceMethod;
140     this.distance = distance;
141     this.twoBodyCutoffDist = twoBodyCutoffDist;
142     this.threeBodyCutoffDist = threeBodyCutoffDist;
143     distanceMatrix();
144   }
145 
146   /**
147    * Returns true if all values (usually distances) are both finite and not set to Double.MAX_VALUE.
148    *
149    * @param values Values to check.
150    * @return If all values are regular, finite values.
151    */
152   private static boolean areFiniteAndNotMax(double... values) {
153     return Arrays.stream(values)
154         .allMatch((double val) -> Double.isFinite(val) && val < Double.MAX_VALUE);
155   }
156 
157   /**
158    * Gets the raw distance between two rotamers using lazy loading of the distance matrix. Intended
159    * uses: helper method for get2BodyDistance, and for checking for superpositions.
160    *
161    * @param i  A residue index.
162    * @param ri A rotamer index for i.
163    * @param j  A residue index j!=i.
164    * @param rj A rotamer index for j.
165    * @return dist(i, ri, j, rj), ignoring distance matrix method used.
166    */
167   public double checkDistMatrix(int i, int ri, int j, int rj) {
168     if (i > j) {
169       int temp = i;
170       i = j;
171       j = temp;
172       temp = ri;
173       ri = rj;
174       rj = temp;
175     }
176     return distanceMatrix[i][ri].getDistance(j, rj);
177   }
178 
179   /**
180    * Checks if the i,ri,j,rj pair exceeds the pair distance thresholds.
181    *
182    * @param i  A residue index.
183    * @param ri A rotamer index for i.
184    * @param j  A residue index j!=i.
185    * @param rj A rotamer index for j.
186    * @return If i,ri,j,rj is greater than the threshold distance.
187    */
188   public boolean checkPairDistThreshold(int i, int ri, int j, int rj) {
189     if (twoBodyCutoffDist <= 0 || !Double.isFinite(twoBodyCutoffDist)) {
190       return false;
191     }
192     return (get2BodyDistance(i, ri, j, rj) > twoBodyCutoffDist);
193   }
194 
195   /**
196    * Checks if the i,ri,j,rj,k,rk,l,rl quad exceeds the 3-body threshold, or if any component exceeds
197    * the pair/triple distance thresholds.
198    *
199    * @param i  A residue index.
200    * @param ri A rotamer index for i.
201    * @param j  A residue index j!=i.
202    * @param rj A rotamer index for j.
203    * @param k  A residue index k!=i, k!=j.
204    * @param rk A rotamer index for k.
205    * @param l  A residue index l!=i, l!=j, l!=k.
206    * @param rl A rotamer index for l.
207    * @return If i,ri,j,rj,k,rk,l,rl is greater than the threshold distances.
208    */
209   public boolean checkQuadDistThreshold(int i, int ri, int j, int rj, int k, int rk, int l, int rl) {
210     if (checkTriDistThreshold(i, ri, j, rj, k, rk) || checkTriDistThreshold(i, ri, j, rj, l, rl)
211         || checkTriDistThreshold(i, ri, k, rk, l, rl) || checkTriDistThreshold(j, rj, k, rk, l,
212         rl)) {
213       return true;
214     }
215     // Use the 3-body cutoff distance for now.
216     if (threeBodyCutoffDist <= 0 || !Double.isFinite(threeBodyCutoffDist)) {
217       return false;
218     }
219     return get4BodyDistance(i, ri, j, rj, k, rk, l, rl) > threeBodyCutoffDist;
220   }
221 
222   /**
223    * Checks if the i,ri,j,rj,k,rk triple exceeds the 3-body threshold, or if any component exceeds
224    * the pair distance threshold.
225    *
226    * @param i  A residue index.
227    * @param ri A rotamer index for i.
228    * @param j  A residue index j!=i.
229    * @param rj A rotamer index for j.
230    * @param k  A residue index k!=i, k!=j.
231    * @param rk A rotamer index for k.
232    * @return If i,ri,j,rj,k,rk is greater than the threshold distances.
233    */
234   public boolean checkTriDistThreshold(int i, int ri, int j, int rj, int k, int rk) {
235     if (checkPairDistThreshold(i, ri, j, rj) || checkPairDistThreshold(i, ri, k, rk)
236         || checkPairDistThreshold(j, rj, k, rk)) {
237       return true;
238     }
239     if (threeBodyCutoffDist <= 0 || !Double.isFinite(threeBodyCutoffDist)) {
240       return false;
241     }
242     return (get3BodyDistance(i, ri, j, rj, k, rk) > threeBodyCutoffDist);
243   }
244 
245   /**
246    * Checks the distance matrix, finding the shortest distance between two residues' rotamers or any
247    * rotamers from two residues under any symmetry operator; will evaluate this if distance matrix
248    * not already filled. Default is to find the shortest distance between two residues rather than
249    * between two rotamers.
250    *
251    * @param i  Residue i
252    * @param ri Rotamer for i
253    * @param j  Residue j
254    * @param rj Rotamer for j
255    * @return Shortest distance
256    */
257   public double get2BodyDistance(int i, int ri, int j, int rj) {
258     if (i > j) {
259       int temp = i;
260       i = j;
261       j = temp;
262       temp = ri;
263       ri = rj;
264       rj = temp;
265     }
266 
267     if (distanceMethod == RotamerOptimization.DistanceMethod.ROTAMER) {
268       return checkDistMatrix(i, ri, j, rj);
269     } else {
270       return getResidueDistance(i, ri, j, rj);
271     }
272   }
273 
274   /**
275    * Returns the RMS separation distance for the closest rotamers of three residues' 2-body
276    * distances. Defaults to Double.MAX_VALUE when there are pair distances outside cutoffs.
277    *
278    * @param i  Residue i
279    * @param ri Rotamer for i
280    * @param j  Residue j
281    * @param rj Rotamer for j
282    * @param k  Residue k
283    * @param rk Rotamer for k
284    * @return RMS separation distance
285    */
286   public double get3BodyResidueDistance(int i, int ri, int j, int rj, int k, int rk) {
287     double ij = getResidueDistance(i, ri, j, rj);
288     double ik = getResidueDistance(i, ri, k, rk);
289     double jk = getResidueDistance(j, rj, k, rk);
290     if (areFiniteAndNotMax(ij, ik, jk)) {
291       return sqrt((ij * ij + ik * ik + jk * jk) / 3.0);
292     } else {
293       return Double.MAX_VALUE;
294     }
295   }
296 
297   /**
298    * Returns the RMS separation distance for the closest rotamers of 6 2-body distances from four
299    * residues. Defaults to Double.MAX_VALUE when there are pair distances outside cutoffs.
300    *
301    * @param i  Residue i
302    * @param ri Rotamer for i
303    * @param j  Residue j
304    * @param rj Rotamer for j
305    * @param k  Residue k
306    * @param rk Rotamer for k
307    * @param l  Residue l
308    * @param rl Rotamer for l
309    * @return RMS separation distance
310    */
311   public double get4BodyResidueDistance(int i, int ri, int j, int rj, int k, int rk, int l, int rl) {
312     double ij = getResidueDistance(i, ri, j, rj);
313     double ik = getResidueDistance(i, ri, k, rk);
314     double il = getResidueDistance(i, ri, l, rl);
315     double jk = getResidueDistance(j, rj, k, rk);
316     double jl = getResidueDistance(j, rj, l, rl);
317     double kl = getResidueDistance(k, rk, l, rl);
318     if (areFiniteAndNotMax(ij, ik, il, jk, jl, kl)) {
319       return sqrt((ij * ij + ik * ik + il * il + jk * jk + jl * jl + kl * kl) / 6.0);
320     } else {
321       return Double.MAX_VALUE;
322     }
323   }
324 
325   /**
326    * Returns the RMS distance between an arbitrary set of rotamers.
327    *
328    * @param resRot Residue index-rotamer index pairs.
329    * @return RMS distance, or Double.MAX_VALUE if ill-defined.
330    */
331   public double getRawNBodyDistance(int... resRot) {
332     int nRes = resRot.length;
333     if (nRes % 2 != 0) {
334       throw new IllegalArgumentException(" Must have an even number of arguments; res-rot pairs!");
335     }
336     nRes /= 2;
337     if (nRes < 2) {
338       throw new IllegalArgumentException(" Must have >= 4 arguments; at least 2 res-rot pairs!");
339     }
340     // nCr where n is # of residues, choose pairs.
341     int numDists = (int) CombinatoricsUtils.binomialCoefficient(nRes, 2);
342     double mult = 1.0 / numDists;
343     double totDist2 = 0.0;
344 
345     for (int i = 0; i < nRes - 1; i++) {
346       int i2 = 2 * i;
347       for (int j = i + 1; j < nRes; j++) {
348         int j2 = 2 * j;
349         double rawDist = checkDistMatrix(resRot[i2], resRot[i2 + 1], resRot[j2], resRot[j2 + 1]);
350         if (!Double.isFinite(rawDist) || rawDist == Double.MAX_VALUE) {
351           return Double.MAX_VALUE;
352         }
353         totDist2 += (rawDist * mult);
354       }
355     }
356     return FastMath.sqrt(totDist2);
357   }
358 
359   /**
360    * Checks the distance matrix, finding the shortest distance between the closest rotamers of two
361    * residues.
362    *
363    * @param i  Residue i
364    * @param ri Rotamer for i
365    * @param j  Residue j
366    * @param rj Rotamer for j
367    * @return Shortest distance
368    */
369   public double getResidueDistance(int i, int ri, int j, int rj) {
370     if (i > j) {
371       int temp = i;
372       i = j;
373       j = temp;
374       temp = ri;
375       ri = rj;
376       rj = temp;
377     }
378 
379     double minDist = Double.MAX_VALUE;
380     final int lenri = distanceMatrix[i].length;
381     final int lenrj = allResiduesArray[j].getRotamers().length;
382     for (int roti = 0; roti < lenri; roti++) {
383       for (int rotj = 0; rotj < lenrj; rotj++) {
384         minDist = Math.min(minDist, checkDistMatrix(i, roti, j, rotj));
385       }
386     }
387     return minDist;
388   }
389 
390   /**
391    * Calculates the minimum distance between two sets of coordinates in a given symmetry operator.
392    *
393    * @param resi  Coordinates of i by [atom][xyz]
394    * @param resj  Coordinates of j by [atom][xyz]
395    * @param symOp Symmetry operator to apply
396    * @return Minimum distance
397    */
398   public double interResidueDistance(double[][] resi, double[][] resj, SymOp symOp) {
399     double dist = Double.MAX_VALUE;
400     Crystal crystal = molecularAssembly.getCrystal();
401     for (double[] xi : resi) {
402       for (double[] xj : resj) {
403         if (symOp != null) {
404           crystal.applySymOp(xj, xj, symOp);
405         }
406         double r = crystal.image(xi[0] - xj[0], xi[1] - xj[1], xi[2] - xj[2]);
407         if (r < dist) {
408           dist = r;
409         }
410       }
411     }
412     return sqrt(dist);
413   }
414 
415   private void distanceMatrix() {
416     distanceMatrix = new NeighborDistances[numResidues - 1][];
417     for (int i = 0; i < (numResidues - 1); i++) {
418       Residue residuei = allResiduesArray[i];
419       int lengthRi;
420       try {
421         lengthRi = residuei.getRotamers().length;
422       } catch (IndexOutOfBoundsException ex) {
423         logger.warning(format(" Residue i %s has null rotamers.", residuei.toFormattedString(false, true)));
424         continue;
425       }
426       distanceMatrix[i] = new NeighborDistances[lengthRi];
427       for (int ri = 0; ri < lengthRi; ri++) {
428         distanceMatrix[i][ri] = new NeighborDistances(i, ri);
429       }
430     }
431 
432     ResidueState[] orig = ResidueState.storeAllCoordinates(allResiduesList);
433     int nMultiRes = 0;
434 
435     // Build a list that contains one atom from each Residue: CA from
436     // amino acids, C1 from nucleic acids, or the first atom otherwise.
437     Atom[] atoms = new Atom[numResidues];
438     for (int i = 0; i < numResidues; i++) {
439       Residue residuei = allResiduesArray[i];
440       atoms[i] = residuei.getReferenceAtom();
441       if (residuei instanceof MultiResidue) {
442         ++nMultiRes;
443       }
444     }
445 
446       /*
447        Use of the pre-existing ParallelTeam causes a conflict when
448        MultiResidues must re-init the force field. Temporary solution
449        for sequence optimization: if > 1 residue optimized, run on only
450        one thread.
451       */
452     ParallelTeam parallelTeam = getParallelTeam(nMultiRes);
453     Crystal crystal = molecularAssembly.getCrystal();
454     int nSymm = crystal.spaceGroup.getNumberOfSymOps();
455     logger.info("\n Computing Residue Distance Matrix");
456 
457     double nlistCutoff = Math.max(Math.max(distance, twoBodyCutoffDist), threeBodyCutoffDist);
458 
459     // Two residues whose c-alphas are separated by 25 angstroms may
460     // still interact if they have long side-chains directed at each other.
461     double conservativeBuffer = 25.0;
462     nlistCutoff += conservativeBuffer;
463 
464       /*
465        For small crystals, including replicate unit cells in the
466        distance matrix is redundant. The cutoff is reduced to the
467        interfacial radius.
468       */
469     if (!crystal.aperiodic()) {
470       double sphere = min(min(crystal.interfacialRadiusA, crystal.interfacialRadiusB), crystal.interfacialRadiusC);
471       if (nlistCutoff > sphere) {
472         nlistCutoff = sphere;
473       }
474     }
475 
476     NeighborList neighborList = new NeighborList(crystal, atoms, nlistCutoff, 0.0, parallelTeam);
477 
478     // Expand coordinates
479     double[][] xyz = new double[nSymm][3 * numResidues];
480     double[] in = new double[3];
481     double[] out = new double[3];
482     for (int iSymOp = 0; iSymOp < nSymm; iSymOp++) {
483       SymOp symOp = crystal.spaceGroup.getSymOp(iSymOp);
484       for (int i = 0; i < numResidues; i++) {
485         Atom atom = atoms[i];
486         in[0] = atom.getX();
487         in[1] = atom.getY();
488         in[2] = atom.getZ();
489         crystal.applySymOp(in, out, symOp);
490         int iX = i * 3;
491         int iY = iX + 1;
492         int iZ = iX + 2;
493         xyz[iSymOp][iX] = out[0];
494         xyz[iSymOp][iY] = out[1];
495         xyz[iSymOp][iZ] = out[2];
496       }
497     }
498 
499     // Build the residue neighbor-list.
500     int[][][] lists = new int[nSymm][numResidues][];
501     boolean forceRebuild = true;
502     boolean printLists = false;
503     long neighborTime = -System.nanoTime();
504     neighborList.buildList(xyz, lists, null, forceRebuild, printLists);
505 
506     neighborTime += System.nanoTime();
507     logger.info(format(" Built residue neighbor list:           %8.3f sec", neighborTime * 1.0e-9));
508 
509     DistanceRegion distanceRegion = new DistanceRegion(parallelTeam.getThreadCount(), numResidues,
510         crystal, lists, neighborList.getPairwiseSchedule());
511 
512     long parallelTime = -System.nanoTime();
513     try {
514       distanceRegion.init(this, molecularAssembly, allResiduesArray, algorithmListener, distanceMatrix);
515       parallelTeam.execute(distanceRegion);
516     } catch (Exception e) {
517       String message = " Exception computing residue distance matrix.";
518       logger.log(Level.SEVERE, message, e);
519     }
520     parallelTime += System.nanoTime();
521     logger.info(format(" Pairwise distance matrix:              %8.3f sec", parallelTime * 1.0e-9));
522 
523     ResidueState.revertAllCoordinates(allResiduesList, orig);
524     try {
525       parallelTeam.shutdown();
526     } catch (Exception ex) {
527       logger.warning(format(" Exception shutting down parallel team for the distance matrix: %s", ex));
528     }
529   }
530 
531   private ParallelTeam getParallelTeam(int nMultiRes) {
532     int nThreads;
533     if (molecularAssembly.getPotentialEnergy().getParallelTeam() != null) {
534       nThreads = (nMultiRes > 1) ? 1
535           : molecularAssembly.getPotentialEnergy().getParallelTeam().getThreadCount();
536     } else {
537       // Suggested: nThreads = (nMultiRes > 1) ? 1 : ParallelTeam.getDefaultThreadCount();
538       nThreads = 16;
539     }
540     ParallelTeam parallelTeam = new ParallelTeam(nThreads);
541     return parallelTeam;
542   }
543 
544   /**
545    * Returns the RMS separation distance of three 2-body distances. Defaults to Double.MAX_VALUE when
546    * there are pair distances outside cutoffs.
547    *
548    * @param i  Residue i
549    * @param ri Rotamer for i
550    * @param j  Residue j
551    * @param rj Rotamer for j
552    * @param k  Residue k
553    * @param rk Rotamer for k
554    * @return RMS separation distance
555    */
556   private double get3BodyDistance(int i, int ri, int j, int rj, int k, int rk) {
557     double ij = get2BodyDistance(i, ri, j, rj);
558     double ik = get2BodyDistance(i, ri, k, rk);
559     double jk = get2BodyDistance(j, rj, k, rk);
560     if (areFiniteAndNotMax(ij, ik, jk)) {
561       return sqrt((ij * ij + ik * ik + jk * jk) / 3.0);
562     } else {
563       return Double.MAX_VALUE;
564     }
565   }
566 
567   /**
568    * Returns the RMS separation distance of 6 2-body distances. Defaults to Double.MAX_VALUE when
569    * there are pair distances outside cutoffs.
570    *
571    * @param i  Residue i
572    * @param ri Rotamer for i
573    * @param j  Residue j
574    * @param rj Rotamer for j
575    * @param k  Residue k
576    * @param rk Rotamer for k
577    * @param l  Residue l
578    * @param rl Rotamer for l
579    * @return RMS separation distance
580    */
581   private double get4BodyDistance(int i, int ri, int j, int rj, int k, int rk, int l, int rl) {
582     double ij = get2BodyDistance(i, ri, j, rj);
583     double ik = get2BodyDistance(i, ri, k, rk);
584     double il = get2BodyDistance(i, ri, l, rl);
585     double jk = get2BodyDistance(j, rj, k, rk);
586     double jl = get2BodyDistance(j, rj, l, rl);
587     double kl = get2BodyDistance(k, rk, l, rl);
588     if (areFiniteAndNotMax(ij, ik, il, jk, jl, kl)) {
589       return sqrt((ij * ij + ik * ik + il * il + jk * jk + jl * jl + kl * kl) / 6.0);
590     } else {
591       return Double.MAX_VALUE;
592     }
593   }
594 
595   /**
596    * Evaluate the pairwise distance between two residues' rotamers under any symmetry operator; does
597    * "lazy loading" for the distance matrix.
598    *
599    * @param i  Residue i
600    * @param ri Rotamer for i
601    * @param j  Residue j
602    * @param rj Rotamer for j
603    * @return Shortest distance
604    */
605   private double evaluateDistance(int i, int ri, int j, int rj) {
606     Residue resi = allResiduesArray[i];
607     Rotamer[] rotamersI = resi.getRotamers();
608     Rotamer roti = rotamersI[ri];
609     double[][] xi;
610     if (roti.equals(resi.getRotamer())) {
611       xi = resi.storeCoordinateArray();
612     } else {
613       ResidueState origI = resi.storeState();
614       RotamerLibrary.applyRotamer(resi, roti);
615       xi = resi.storeCoordinateArray();
616       resi.revertState(origI);
617     }
618 
619     Residue resj = allResiduesArray[j];
620     Rotamer[] rotamersJ = resj.getRotamers();
621     Rotamer rotj = rotamersJ[rj];
622     double[][] xj;
623     if (rotj.equals(resj.getRotamer())) {
624       xj = resj.storeCoordinateArray();
625     } else {
626       ResidueState origJ = resj.storeState();
627       RotamerLibrary.applyRotamer(resj, rotj);
628       xj = resj.storeCoordinateArray();
629       resj.revertState(origJ);
630     }
631 
632     Crystal crystal = molecularAssembly.getCrystal();
633     int nSymm = crystal.spaceGroup.getNumberOfSymOps();
634     double minDist = Double.MAX_VALUE;
635     for (int iSymOp = 0; iSymOp < nSymm; iSymOp++) {
636       SymOp symOp = crystal.spaceGroup.getSymOp(iSymOp);
637       double dist = interResidueDistance(xi, xj, symOp);
638       minDist = Math.min(dist, minDist);
639     }
640     return minDist;
641   }
642 
643   /**
644    * Store all neighbors of a residue / rotamer pair.
645    */
646   public class NeighborDistances {
647 
648     /**
649      * The residue index.
650      */
651     private final int i;
652     /**
653      * The rotamer index.
654      */
655     private final int ri;
656 
657     /**
658      * Map of distances.
659      * <p>
660      * The key is the index of residue j.
661      * The value is an array of distances for each rotamer of residue j.
662      */
663     private final Map<Integer, double[]> distances = new HashMap<>();
664 
665     /**
666      * Constructor.
667      *
668      * @param i  The residue index.
669      * @param ri The rotamer index.
670      */
671     public NeighborDistances(int i, int ri) {
672       this.i = i;
673       this.ri = ri;
674     }
675 
676     /**
677      * Store a distance.
678      *
679      * @param j        The residue index.
680      * @param rj       The rotamer index.
681      * @param distance The distance.
682      */
683     public void storeDistance(int j, int rj, double distance) {
684       // Synchronize on the distances map to avoid concurrent modification.
685       synchronized (distances) {
686         if (distances.containsKey(j)) {
687           distances.get(j)[rj] = distance;
688         } else {
689           double[] dists = new double[allResiduesArray[j].getRotamers().length];
690           Arrays.fill(dists, -1);
691           dists[rj] = distance;
692           distances.put(j, dists);
693         }
694       }
695     }
696 
697     /**
698      * Get a distance.
699      *
700      * @param j  The residue index.
701      * @param rj The rotamer index.
702      * @return The distance.
703      */
704     public double getDistance(int j, int rj) {
705       if (distances.containsKey(j) && distances.get(j)[rj] >= 0) {
706         return distances.get(j)[rj];
707       } else {
708         double distance = Double.POSITIVE_INFINITY;
709         storeDistance(j, rj, distance);
710         return distance;
711       }
712     }
713   }
714 
715 }