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