diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/asa/AsaCalculator.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/asa/AsaCalculator.java
index 0f46b7f416..ed576b9abe 100644
--- a/biojava-structure/src/main/java/org/biojava/nbio/structure/asa/AsaCalculator.java
+++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/asa/AsaCalculator.java
@@ -27,7 +27,6 @@
import org.slf4j.LoggerFactory;
import javax.vecmath.Point3d;
-import javax.vecmath.Vector3d;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -101,12 +100,56 @@ public void run() {
}
}
- static class IndexAndDistance {
- final int index;
- final double dist;
- IndexAndDistance(int index, double dist) {
- this.index = index;
- this.dist = dist;
+ /**
+ * The neighbors of a single atom, as parallel primitive arrays of neighbor atom indices and their distances to
+ * the central atom, sorted by increasing distance.
+ *
+ * Parallel primitive arrays are used rather than an array of index-distance objects because there are ~30
+ * neighbors per atom: for a large structure that would mean millions of small short-lived objects.
+ */
+ static class Neighbors {
+
+ /** The neighbor atom indices, ordered by increasing distance to the central atom */
+ final int[] indices;
+ /** The distances to the central atom, in increasing order and parallel to {@link #indices} */
+ final double[] dists;
+
+ private Neighbors(int[] indices, double[] dists) {
+ this.indices = indices;
+ this.dists = dists;
+ }
+
+ /**
+ * Creates a Neighbors from the first count elements of the given buffers, copying them to exact-size arrays
+ * and sorting them by increasing distance.
+ * @param indicesBuffer the neighbor indices, only the first count elements are used
+ * @param distsBuffer the neighbor distances, only the first count elements are used
+ * @param count the number of neighbors
+ * @return the sorted neighbors
+ */
+ static Neighbors createSorted(int[] indicesBuffer, double[] distsBuffer, int count) {
+ int[] indices = Arrays.copyOf(indicesBuffer, count);
+ double[] dists = Arrays.copyOf(distsBuffer, count);
+ // Sorting by closest to farthest away neighbors achieves faster runtimes when checking for occluded
+ // sphere sample points in calcSingleAsa. This follows the ideas exposed in
+ // Eisenhaber et al, J Comp Chemistry 1994 (https://onlinelibrary.wiley.com/doi/epdf/10.1002/jcc.540160303)
+ // This is essential for performance: it brings down the number of occlusion checks to
+ // an average of n_sphere_points/10 per atom, producing ~ x4 performance gain overall.
+ // An insertion sort is used because the arrays are small (~30 elements on average) and because it avoids
+ // both the boxing of a comparator-based sort and the object allocation an index-distance array would need.
+ for (int i = 1; i < count; i++) {
+ double dist = dists[i];
+ int index = indices[i];
+ int j = i - 1;
+ while (j >= 0 && dists[j] > dist) {
+ dists[j + 1] = dists[j];
+ indices[j + 1] = indices[j];
+ j--;
+ }
+ dists[j + 1] = dist;
+ indices[j + 1] = index;
+ }
+ return new Neighbors(indices, dists);
}
}
@@ -116,9 +159,16 @@ static class IndexAndDistance {
private final double[] radii;
private final double probe;
private final int nThreads;
- private Vector3d[] spherePoints;
+ /**
+ * The sphere points to sample, as a flat array of interleaved x,y,z coordinates (thus of size 3 x nSpherePoints).
+ * A flat array of primitives (rather than an array of Vector3d objects) is used for performance: it keeps the
+ * points contiguous in memory and avoids a pointer dereference per point in the innermost loop of
+ * {@link #calcSingleAsa(int)}.
+ */
+ private double[] spherePoints;
+ private int nSpherePoints;
private double cons;
- private IndexAndDistance[][] neighborIndices;
+ private Neighbors[] neighbors;
private boolean useSpatialHashingForNeighbors;
@@ -239,7 +289,8 @@ private void initSpherePoints(int nSpherePoints) {
logger.debug("Will use {} sphere points", nSpherePoints);
// initialising the sphere points to sample
- spherePoints = generateSpherePoints(nSpherePoints);
+ this.nSpherePoints = nSpherePoints;
+ this.spherePoints = generateSpherePoints(nSpherePoints);
cons = 4.0 * Math.PI / nSpherePoints;
}
@@ -285,10 +336,10 @@ public double[] calculateAsas() {
long start = System.currentTimeMillis();
if (useSpatialHashingForNeighbors) {
logger.debug("Will use spatial hashing to find neighbors");
- neighborIndices = findNeighborIndicesSpatialHashing();
+ neighbors = findNeighborIndicesSpatialHashing();
} else {
logger.debug("Will not use spatial hashing to find neighbors");
- neighborIndices = findNeighborIndices();
+ neighbors = findNeighborIndices();
}
long end = System.currentTimeMillis();
logger.debug("Took {} s to find neighbors", (end-start)/1000.0);
@@ -334,109 +385,126 @@ void setUseSpatialHashingForNeighbors(boolean useSpatialHashingForNeighbors) {
* Returns list of 3d coordinates of points on a unit sphere using the
* Golden Section Spiral algorithm.
* @param nSpherePoints the number of points to be used in generating the spherical dot-density
- * @return the array of points as Vector3d objects
+ * @return a flat array of interleaved x,y,z coordinates, of size 3 x nSpherePoints
*/
- private Vector3d[] generateSpherePoints(int nSpherePoints) {
- Vector3d[] points = new Vector3d[nSpherePoints];
+ private double[] generateSpherePoints(int nSpherePoints) {
+ double[] points = new double[3 * nSpherePoints];
double inc = Math.PI * (3.0 - Math.sqrt(5.0));
double offset = 2.0 / nSpherePoints;
for (int k=0;k thisNbIndices = new ArrayList<>(initialCapacity);
+ int count = 0;
for (int i = 0; i < atomCoords.length; i++) {
if (i == k) continue;
double dist = atomCoords[i].distance(atomCoords[k]);
- if (dist < radius + radii[i]) {
- thisNbIndices.add(new IndexAndDistance(i, dist));
+ if (areNeighbors(k, i, dist)) {
+ if (count == indicesBuffer.length) {
+ indicesBuffer = Arrays.copyOf(indicesBuffer, count * 2);
+ distsBuffer = Arrays.copyOf(distsBuffer, count * 2);
+ }
+ indicesBuffer[count] = i;
+ distsBuffer[count] = dist;
+ count++;
}
}
- IndexAndDistance[] indicesArray = thisNbIndices.toArray(new IndexAndDistance[0]);
- nbsIndices[k] = indicesArray;
+ nbs[k] = Neighbors.createSorted(indicesBuffer, distsBuffer, count);
}
- return nbsIndices;
+ return nbs;
}
/**
- * Returns the 2-dimensional array with neighbor indices for every atom,
+ * Returns the neighbors of every atom, sorted by increasing distance,
* using spatial hashing to avoid all to all distance calculation.
- * @return 2-dimensional array of size: n_atoms x n_neighbors_per_atom
+ * @return array of size n_atoms
*/
- IndexAndDistance[][] findNeighborIndicesSpatialHashing() {
-
- // looking at a typical protein case, number of neighbours are from ~10 to ~50, with an average of ~30
- int initialCapacity = 60;
+ Neighbors[] findNeighborIndicesSpatialHashing() {
List contactList = calcContacts();
- Map> indices = new HashMap<>(atomCoords.length);
+
+ // A first pass to count the neighbors per atom, so that exact-size arrays can be allocated in the second
+ // pass. Atom indices are dense, so plain arrays are used rather than a map: that avoids boxing the indices
+ // and the repeated hashing, which are significant given that there are ~30 contacts per atom.
+ int[] counts = new int[atomCoords.length];
for (Contact contact : contactList) {
// note contacts are stored 1-way only, with j>i
int i = contact.getI();
int j = contact.getJ();
-
- List iIndices;
- List jIndices;
- if (!indices.containsKey(i)) {
- iIndices = new ArrayList<>(initialCapacity);
- indices.put(i, iIndices);
- } else {
- iIndices = indices.get(i);
- }
- if (!indices.containsKey(j)) {
- jIndices = new ArrayList<>(initialCapacity);
- indices.put(j, jIndices);
- } else {
- jIndices = indices.get(j);
- }
-
- double radius = radii[i] + probe + probe;
- double dist = contact.getDistance();
- if (dist < radius + radii[j]) {
- iIndices.add(new IndexAndDistance(j, dist));
- jIndices.add(new IndexAndDistance(i, dist));
+ if (areNeighbors(i, j, contact.getDistance())) {
+ counts[i]++;
+ counts[j]++;
}
}
- // convert map to array for fast access
- IndexAndDistance[][] nbsIndices = new IndexAndDistance[atomCoords.length][];
- for (Map.Entry> entry : indices.entrySet()) {
- List list = entry.getValue();
- IndexAndDistance[] indexAndDistances = list.toArray(new IndexAndDistance[0]);
- nbsIndices[entry.getKey()] = indexAndDistances;
+ int[][] indices = new int[atomCoords.length][];
+ double[][] dists = new double[atomCoords.length][];
+ for (int i = 0; i < atomCoords.length; i++) {
+ // note that some atoms might have no neighbors at all, in which case these are empty arrays
+ indices[i] = new int[counts[i]];
+ dists[i] = new double[counts[i]];
}
- // important: some atoms might have no neighbors at all: we need to initialise to empty arrays
- for (int i=0; i calcContacts() {
private double calcSingleAsa(int i) {
Point3d atom_i = atomCoords[i];
- int n_neighbor = neighborIndices[i].length;
- IndexAndDistance[] neighbor_indices = neighborIndices[i];
- // Sorting by closest to farthest away neighbors achieves faster runtimes when checking for occluded
- // sphere sample points below. This follows the ideas exposed in
- // Eisenhaber et al, J Comp Chemistry 1994 (https://onlinelibrary.wiley.com/doi/epdf/10.1002/jcc.540160303)
- // This is essential for performance. In my tests this brings down the number of occlusion checks in loop below to
- // an average of n_sphere_points/10 per atom i, producing ~ x4 performance gain overall
- Arrays.sort(neighbor_indices, Comparator.comparingDouble(o -> o.dist));
+ // note the neighbors are already sorted by increasing distance (see Neighbors#createSorted), which is
+ // essential for the performance of the occlusion checks in the loop below
+ Neighbors nbs = neighbors[i];
+ int[] neighbor_indices = nbs.indices;
+ double[] neighbor_dists = nbs.dists;
+ int n_neighbor = neighbor_indices.length;
double radius_i = probe + radii[i];
@@ -476,36 +542,47 @@ private double calcSingleAsa(int i) {
int[] numDistsCalced = null;
if (logger.isDebugEnabled()) numDistsCalced = new int[n_neighbor];
- // now we precalculate anything depending only on i,j in equation 3 in Eisenhaber 1994
- double[] sqRadii = new double[n_neighbor];
- Vector3d[] aj_minus_ais = new Vector3d[n_neighbor];
+ // Now we precalculate anything depending only on i,j in equation 3 in Eisenhaber 1994.
+ // The per-neighbor data is laid out in a single flat array as quadruplets
+ // [aj_minus_ai.x, aj_minus_ai.y, aj_minus_ai.z, cutoff], so that the innermost loop below is a purely
+ // sequential scan over contiguous primitives, with no object dereferencing. That matches the access
+ // pattern of the early break and is significantly faster than an array of Vector3d objects.
+ double[] nbData = new double[4 * n_neighbor];
for (int nbArrayInd =0; nbArrayInd> 2]++;
- if (dotProd > sqRadii[nbArrayInd]) {
+ if (dotProd > nbData[off + 3]) {
is_accessible = false;
break;
}
diff --git a/biojava-structure/src/test/java/org/biojava/nbio/structure/asa/TestAsaCalc.java b/biojava-structure/src/test/java/org/biojava/nbio/structure/asa/TestAsaCalc.java
index 7be56be156..c6a268f1f5 100644
--- a/biojava-structure/src/test/java/org/biojava/nbio/structure/asa/TestAsaCalc.java
+++ b/biojava-structure/src/test/java/org/biojava/nbio/structure/asa/TestAsaCalc.java
@@ -126,19 +126,19 @@ public void testNeighborIndicesFinding() throws StructureException, IOException
AsaCalculator.DEFAULT_PROBE_SIZE,
1000, 1, false);
- AsaCalculator.IndexAndDistance[][] allNbsSh = asaCalc.findNeighborIndicesSpatialHashing();
+ AsaCalculator.Neighbors[] allNbsSh = asaCalc.findNeighborIndicesSpatialHashing();
- AsaCalculator.IndexAndDistance[][] allNbs = asaCalc.findNeighborIndices();
+ AsaCalculator.Neighbors[] allNbs = asaCalc.findNeighborIndices();
for (int indexToTest =0; indexToTest < asaCalc.getAtomCoords().length; indexToTest++) {
//int indexToTest = 198;
- AsaCalculator.IndexAndDistance[] nbsSh = allNbsSh[indexToTest];
- AsaCalculator.IndexAndDistance[] nbs = allNbs[indexToTest];
+ int[] nbsSh = allNbsSh[indexToTest].indices;
+ int[] nbs = allNbs[indexToTest].indices;
List listOfMatchingIndices = new ArrayList<>();
for (int i = 0; i < nbsSh.length; i++) {
for (int j = 0; j < nbs.length; j++) {
- if (nbs[j].index == nbsSh[i].index) {
+ if (nbs[j] == nbsSh[i]) {
listOfMatchingIndices.add(j);
break;
}
@@ -229,21 +229,21 @@ public void testNoNeighborsIssue() {
AsaCalculator.DEFAULT_PROBE_SIZE,
1000, 1);
- AsaCalculator.IndexAndDistance[][] allNbsSh = asaCalc.findNeighborIndicesSpatialHashing();
+ AsaCalculator.Neighbors[] allNbsSh = asaCalc.findNeighborIndicesSpatialHashing();
- AsaCalculator.IndexAndDistance[][] allNbs = asaCalc.findNeighborIndices();
+ AsaCalculator.Neighbors[] allNbs = asaCalc.findNeighborIndices();
assertEquals(3, allNbs.length);
assertEquals(3, allNbsSh.length);
for (int indexToTest =0; indexToTest < asaCalc.getAtomCoords().length; indexToTest++) {
- AsaCalculator.IndexAndDistance[] nbsSh = allNbsSh[indexToTest];
- AsaCalculator.IndexAndDistance[] nbs = allNbs[indexToTest];
+ int[] nbsSh = allNbsSh[indexToTest].indices;
+ int[] nbs = allNbs[indexToTest].indices;
List listOfMatchingIndices = new ArrayList<>();
for (int i = 0; i < nbsSh.length; i++) {
for (int j = 0; j < nbs.length; j++) {
- if (nbs[j].index == nbsSh[i].index) {
+ if (nbs[j] == nbsSh[i]) {
listOfMatchingIndices.add(j);
break;
}
@@ -256,7 +256,7 @@ public void testNoNeighborsIssue() {
}
// first atom should have no neighbors
- assertEquals(0, allNbsSh[0].length);
+ assertEquals(0, allNbsSh[0].indices.length);
}
private Atom getAtom(double x, double y, double z) {