diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java index 34de552786..9748065524 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/AtomContactSet.java @@ -28,6 +28,22 @@ /** * A set of atom-atom contacts to hold the results of intra and inter-chain contact calculations + *

+ * Contacts are keyed by the ordered pair of {@link AtomIdentifier}s of the 2 atoms, i.e. the + * pair (a,b) and the pair (b,a) are 2 different keys. Thus look-ups ({@link #hasContact(Atom, Atom)}, + * {@link #getContact(Atom, Atom)}) must give the 2 atoms in the same order in which the contact was + * calculated. The order produced by the calculation ({@link Grid}) is: + *

+ *

+ * Note that the order is not the order of PDB serials or of any other property of the atoms + * themselves: it is only the order in which the atoms were given to the calculation. * * @author duarte_j * @@ -37,6 +53,12 @@ public class AtomContactSet implements Serializable, Iterable { private static final long serialVersionUID = 1L; + /** + * The default load factor of a {@link HashMap}, needed to size the map from an expected number of + * entries. + */ + private static final float DEFAULT_LOAD_FACTOR = 0.75f; + private HashMap, AtomContact> contacts; private double cutoff; @@ -45,31 +67,79 @@ public AtomContactSet(double cutoff) { this.contacts = new HashMap<>(); } + /** + * Creates an AtomContactSet sized to hold the given number of contacts, so that the underlying map + * doesn't have to be repeatedly resized and rehashed as contacts are added. Contact calculations + * produce hundreds of thousands of contacts for a large structure, where the repeated rehashing + * that growing from the default capacity entails is a significant part of the cost. + * @param cutoff the distance cutoff + * @param expectedSize the number of contacts expected to be added. Only affects performance: an + * over-estimate merely leaves the map larger than it needs to be. + */ + public AtomContactSet(double cutoff, int expectedSize) { + this.cutoff = cutoff; + this.contacts = new HashMap<>((int) (expectedSize / DEFAULT_LOAD_FACTOR) + 1); + } + + /** + * Adds the given contact to this set, keyed by the ordered pair of its 2 atoms. If a contact + * with the same ordered pair of atoms is already present it is replaced. + * @param contact the contact to add + */ public void add(AtomContact contact) { this.contacts.put(getAtomIdPairFromContact(contact), contact); } + /** + * Adds all given contacts to this set, see {@link #add(AtomContact)}. + * @param list the contacts to add + */ public void addAll(Collection list) { for (AtomContact contact:list) { this.contacts.put(getAtomIdPairFromContact(contact), contact); } } + /** + * Tells whether a contact exists between the 2 given atoms, in the given order. + *

+ * The 2 atoms have to be passed in the same order in which the contacts of this set were + * calculated, otherwise this returns false even if the 2 atoms are within the distance cutoff. + * See the class documentation for the ordering convention. If the order is not known, both orders + * have to be queried. + * @param atom1 the first atom of the pair + * @param atom2 the second atom of the pair + * @return true if the 2 atoms are in contact in the given order, false otherwise + * @see #getContact(Atom, Atom) + */ public boolean hasContact(Atom atom1, Atom atom2) { return hasContact( new AtomIdentifier(atom1.getPDBserial(),atom1.getGroup().getChainId()), new AtomIdentifier(atom2.getPDBserial(),atom2.getGroup().getChainId()) ); } + /** + * Tells whether a contact exists between the 2 given atom identifiers, in the given order, + * see {@link #hasContact(Atom, Atom)}. + * @param atomId1 the identifier of the first atom of the pair + * @param atomId2 the identifier of the second atom of the pair + * @return true if the 2 atoms are in contact in the given order, false otherwise + */ public boolean hasContact(AtomIdentifier atomId1, AtomIdentifier atomId2) { return contacts.containsKey(new Pair(atomId1,atomId2)); } /** - * Returns the corresponding AtomContact or null if no contact exists between the 2 given atoms - * @param atom1 - * @param atom2 - * @return + * Returns the contact between the 2 given atoms in the given order, or null if there is + * no such contact in this set. + *

+ * As in {@link #hasContact(Atom, Atom)} the order of the 2 atoms matters: they have to be passed + * in the same order in which the contacts of this set were calculated, otherwise null is returned + * even if the 2 atoms are within the distance cutoff. See the class documentation for the + * ordering convention. + * @param atom1 the first atom of the pair + * @param atom2 the second atom of the pair + * @return the contact between the 2 atoms in the given order, or null if there is none */ public AtomContact getContact(Atom atom1, Atom atom2) { return contacts.get(new Pair( diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java index 0047385ab2..ba2fd08cdb 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/Grid.java @@ -66,6 +66,7 @@ public class Grid { private GridCell[][][] cells; private double cutoff; + private double cutoffSq; private int cellSize; private Point3d[] iAtoms; @@ -91,6 +92,7 @@ public class Grid { */ public Grid(double cutoff) { this.cutoff = cutoff; + this.cutoffSq = cutoff * cutoff; this.cellSize = (int) Math.floor(cutoff*SCALE); this.noOverlap = false; } @@ -380,10 +382,12 @@ private int[] getIntBounds(BoundingBox coordbounds) { */ public AtomContactSet getAtomContacts() { - AtomContactSet contacts = new AtomContactSet(cutoff); - List list = getIndicesContacts(); + // each contact maps to at most one entry in the set, so the number of index contacts sizes it + // without ever under-allocating + AtomContactSet contacts = new AtomContactSet(cutoff, list.size()); + if (jAtomObjects == null) { for (Contact cont : list) { contacts.add(new AtomContact(new Pair(iAtomObjects[cont.getI()],iAtomObjects[cont.getJ()]),cont.getDistance())); @@ -496,6 +500,15 @@ public double getCutoff() { return cutoff; } + /** + * Returns the square of the cutoff, precomputed at construction. Used by {@link GridCell} to + * compare squared distances, avoiding a square root per candidate pair. + * @return the squared cutoff + */ + protected double getCutoffSq() { + return cutoffSq; + } + /** * Tells whether (after having added atoms to grid) the i and j grids are not overlapping. * Overlap is defined as enclosing bounds of the 2 grids being no more than one cell size apart. diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java index 6028110eb6..9c75fa1c75 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/GridCell.java @@ -21,6 +21,7 @@ package org.biojava.nbio.structure.contact; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.vecmath.Point3d; @@ -35,30 +36,61 @@ public class GridCell { + /** + * Shared empty array so that cells that never receive indices (e.g. the j indices when only one + * set of atoms was added to the grid) don't allocate anything at all. + */ + private static final int[] EMPTY = new int[0]; + + /** + * Capacity of the index arrays on first insertion. Cell occupancy depends on the cutoff (the cell + * side is the cutoff), ranging from a handful of atoms for small cutoffs to a few tens for large + * ones, so we start small and grow geometrically. + */ + private static final int INITIAL_CAPACITY = 8; + private Grid grid; - private ArrayList iIndices; - private ArrayList jIndices; + + /** + * The indices of the i atoms in this cell, held as a primitive array to avoid the boxing (and the + * pointer chasing it entails) of a Collection of Integers: these are read in the innermost loop of + * the contact calculation. Only the first {@link #numIindices} elements are meaningful. + */ + private int[] iIndices; + private int numIindices; + + /** + * The indices of the j atoms in this cell. See {@link #iIndices}. + */ + private int[] jIndices; + private int numJindices; public GridCell(Grid parent){ - iIndices = new ArrayList<>(); - jIndices = new ArrayList<>(); + iIndices = EMPTY; + jIndices = EMPTY; this.grid = parent; } public void addIindex(int serial){ - iIndices.add(serial); + if (numIindices == iIndices.length) { + iIndices = Arrays.copyOf(iIndices, numIindices == 0 ? INITIAL_CAPACITY : numIindices * 2); + } + iIndices[numIindices++] = serial; } public void addJindex(int serial){ - jIndices.add(serial); + if (numJindices == jIndices.length) { + jIndices = Arrays.copyOf(jIndices, numJindices == 0 ? INITIAL_CAPACITY : numJindices * 2); + } + jIndices[numJindices++] = serial; } public int getNumIindices() { - return iIndices.size(); + return numIindices; } public int getNumJindices() { - return jIndices.size(); + return numJindices; } /** @@ -74,23 +106,31 @@ public List getContactsWithinCell(){ Point3d[] iAtoms = grid.getIAtoms(); Point3d[] jAtoms = grid.getJAtoms(); - double cutoff = grid.getCutoff(); + // we compare squared distances to the squared cutoff, so that the expensive square root is + // only computed for the pairs that are actually in contact (the large majority are not) + double cutoffSq = grid.getCutoffSq(); if (jAtoms==null) { - for (int i:iIndices) { - for (int j:iIndices) { + for (int a=0; ai) { - double distance = iAtoms[i].distance(iAtoms[j]); - if (distance getContactsToOtherCell(GridCell otherCell){ Point3d[] iAtoms = grid.getIAtoms(); Point3d[] jAtoms = grid.getJAtoms(); - double cutoff = grid.getCutoff(); + // we compare squared distances to the squared cutoff, so that the expensive square root is + // only computed for the pairs that are actually in contact (the large majority are not) + double cutoffSq = grid.getCutoffSq(); if (jAtoms==null) { - for (int i:iIndices) { - for (int j:otherCell.iIndices) { + int[] otherIndices = otherCell.iIndices; + int otherNum = otherCell.numIindices; + for (int a=0; ai) { - double distance = iAtoms[i].distance(iAtoms[j]); - if (distance getContactsToOtherCell(GridCell otherCell){ * @return */ public boolean hasContactToAtom(Point3d[] iAtoms, Point3d[] jAtoms, Point3d query, double cutoff) { - for( int i : iIndices ) { - double distance = iAtoms[i].distance(query); - if( distance