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 dc34ff8782..108cea6067 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 @@ -21,12 +21,13 @@ package org.biojava.nbio.structure.asa; import org.biojava.nbio.structure.*; +import org.biojava.nbio.structure.contact.Contact; +import org.biojava.nbio.structure.contact.Grid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.vecmath.Point3d; -import java.util.ArrayList; -import java.util.TreeMap; +import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -36,28 +37,34 @@ /** * Class to calculate Accessible Surface Areas based on * the rolling ball algorithm by Shrake and Rupley. - * + *

* The code is adapted from a python implementation at http://boscoh.com/protein/asapy * (now source is available at https://github.com/boscoh/asa). * Thanks to Bosco K. Ho for a great piece of code and for his fantastic blog. - * + *

* See * Shrake, A., and J. A. Rupley. "Environment and Exposure to Solvent of Protein Atoms. * Lysozyme and Insulin." JMB (1973) 79:351-371. * Lee, B., and Richards, F.M. "The interpretation of Protein Structures: Estimation of * Static Accessibility" JMB (1971) 55:379-400 - * @author duarte_j + * + * @author Jose Duarte * */ public class AsaCalculator { private static final Logger logger = LoggerFactory.getLogger(AsaCalculator.class); - // Bosco uses as default 960, Shrake and Rupley seem to use in their paper 92 (not sure if this is actually the same parameter) - public static final int DEFAULT_N_SPHERE_POINTS = 960; + /** + * The default value for number of sphere points to sample. + * See this paper for a nice study on the effect of this parameter: https://f1000research.com/articles/5-189/v1 + */ + public static final int DEFAULT_N_SPHERE_POINTS = 1000; public static final double DEFAULT_PROBE_SIZE = 1.4; public static final int DEFAULT_NTHREADS = 1; + private static final boolean DEFAULT_USE_SPATIAL_HASHING = true; + // Chothia's amino acid atoms vdw radii @@ -101,15 +108,19 @@ public void run() { private int nThreads; private Point3d[] spherePoints; private double cons; + private int[][] neighborIndices; + + private boolean useSpatialHashingForNeighbors; /** * Constructs a new AsaCalculator. Subsequently call {@link #calculateAsas()} * or {@link #getGroupAsas()} to calculate the ASAs * Only non-Hydrogen atoms are considered in the calculation. - * @param structure - * @param probe - * @param nSpherePoints - * @param nThreads + * @param structure the structure, all non-H atoms will be used + * @param probe the probe size + * @param nSpherePoints the number of points to be used in generating the spherical + * dot-density, the more points the more accurate (and slower) calculation + * @param nThreads the number of parallel threads to use for the calculation * @param hetAtoms if true HET residues are considered, if false they aren't, equivalent to * NACCESS' -h option * @see StructureTools#getAllNonHAtomArray @@ -120,16 +131,15 @@ public AsaCalculator(Structure structure, double probe, int nSpherePoints, int n this.probe = probe; this.nThreads = nThreads; + this.useSpatialHashingForNeighbors = DEFAULT_USE_SPATIAL_HASHING; + // initialising the radii by looking them up through AtomRadii radii = new double[atomCoords.length]; for (int i=0;i asas = new TreeMap(); + TreeMap asas = new TreeMap<>(); double[] asasPerAtom = calculateAsas(); @@ -238,12 +256,26 @@ public double[] calculateAsas() { double[] asas = new double[atomCoords.length]; + long start = System.currentTimeMillis(); + if (useSpatialHashingForNeighbors) { + logger.debug("Will use spatial hashing to find neighbors"); + neighborIndices = findNeighborIndicesSpatialHashing(); + } else { + logger.debug("Will not use spatial hashing to find neighbors"); + neighborIndices = findNeighborIndices(); + } + long end = System.currentTimeMillis(); + logger.debug("Took {} s to find neighbors", (end-start)/1000.0); + + start = System.currentTimeMillis(); if (nThreads<=1) { // (i.e. it will also be 1 thread if 0 or negative number specified) + logger.debug("Will use 1 thread for ASA calculation"); for (int i=0;i neighbor_indices = new ArrayList<>(40); + int initialCapacity = 60; - double radius = radii[k] + probe + probe; + int[][] nbsIndices = new int[atomCoords.length][]; - for (int i=0;i thisNbIndices = new ArrayList<>(initialCapacity); + + 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(i); + } + } + + int[] indicesArray = new int[thisNbIndices.size()]; + for (int i=0;i contactList = calcContacts(); + Map> indices = new HashMap<>(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); + } - if (dist < radius + radii[i]) { - neighbor_indices.add(i); + double radius = radii[i] + probe + probe; + double dist = contact.getDistance(); + if (dist < radius + radii[j]) { + iIndices.add(j); + jIndices.add(i); } + } + // convert map to array for fast access + int[][] nbsIndices = new int[atomCoords.length][]; + for (Map.Entry> entry : indices.entrySet()) { + List list = entry.getValue(); + int[] indicesArray = new int[list.size()]; + for (int i=0;i calcContacts() { + double maxRadius = maxValue(radii); + double cutoff = maxRadius + maxRadius + probe + probe; + logger.debug("Max radius is {}, cutoff is {}", maxRadius, cutoff); + Grid grid = new Grid(cutoff); + grid.addCoords(atomCoords); + return grid.getIndicesContacts(); + } + + private static double maxValue(double[] array) { + double max = array[0]; + for (int i = 0; i < array.length; i++) { + if (array[i] > max) { + max = array[i]; + } + } + return max; } private double calcSingleAsa(int i) { Point3d atom_i = atomCoords[i]; - Integer[] neighbor_indices = findNeighborIndices(i); - int n_neighbor = neighbor_indices.length; + + int n_neighbor = neighborIndices[i].length; + int[] neighbor_indices = neighborIndices[i]; int j_closest_neighbor = 0; double radius = probe + radii[i]; @@ -497,7 +623,7 @@ private static double getRadiusForNucl(NucleotideImpl nuc, Atom atom) { * * If atom is neither part of a nucleotide nor of a standard aminoacid, * the default vdw radius for the element is returned. If atom is of - * unknown type (element) the vdw radius of {@link #Element().N} is returned + * unknown type (element) the vdw radius of {@link Element().N} is returned * * @param atom * @return diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterface.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterface.java index ac319fd95f..9e6dcaf1b3 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterface.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterface.java @@ -209,8 +209,8 @@ protected void setAsas(double[] asas1, double[] asas2, int nSpherePoints, int nT throw new IllegalArgumentException("The size of ASAs of complex doesn't match that of ASAs 1 + ASAs 2"); - groupAsas1 = new TreeMap(); - groupAsas2 = new TreeMap(); + groupAsas1 = new TreeMap<>(); + groupAsas2 = new TreeMap<>(); this.totalArea = 0; diff --git a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterfaceList.java b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterfaceList.java index 9b4144d96a..b5785b514b 100644 --- a/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterfaceList.java +++ b/biojava-structure/src/main/java/org/biojava/nbio/structure/contact/StructureInterfaceList.java @@ -35,7 +35,7 @@ /** * A list of interfaces between 2 molecules (2 sets of atoms) * - * @author duarte_j + * @author Jose Duarte * */ public class StructureInterfaceList implements Serializable, Iterable { @@ -126,8 +126,8 @@ public void calcAsas(int nSpherePoints, int nThreads, int cofactorSizeToUse) { // we get discrepancies (not very big but annoying) which lead to things like negative (small) bsa values - Map uniqAsaChains = new TreeMap(); - Map chainAsas = new TreeMap(); + Map uniqAsaChains = new TreeMap<>(); + Map chainAsas = new TreeMap<>(); // first we gather rotation-unique chains (in terms of AU id and transform id) for (StructureInterface interf:list) { @@ -138,11 +138,15 @@ public void calcAsas(int nSpherePoints, int nThreads, int cofactorSizeToUse) { uniqAsaChains.put(molecId2, interf.getSecondAtomsForAsa(cofactorSizeToUse)); } + logger.debug("Will calculate uncomplexed ASA for {} orientation-unique chains.", uniqAsaChains.size()); + long start = System.currentTimeMillis(); // we only need to calculate ASA for that subset (any translation of those will have same values) for (String molecId:uniqAsaChains.keySet()) { + logger.debug("Calculating uncomplexed ASA for molecId {}, with {} atoms", molecId, uniqAsaChains.get(molecId).length); + AsaCalculator asaCalc = new AsaCalculator(uniqAsaChains.get(molecId), AsaCalculator.DEFAULT_PROBE_SIZE, nSpherePoints, nThreads); @@ -153,8 +157,9 @@ public void calcAsas(int nSpherePoints, int nThreads, int cofactorSizeToUse) { } long end = System.currentTimeMillis(); - logger.debug("Calculated uncomplexed ASA for "+uniqAsaChains.size()+" orientation-unique chains. " - + "Time: "+((end-start)/1000.0)+" s"); + logger.debug("Calculated uncomplexed ASA for {} orientation-unique chains. Time: {} s", uniqAsaChains.size(), ((end-start)/1000.0)); + + logger.debug ("Will calculate complexed ASA for {} pairwise complexes.", list.size()); start = System.currentTimeMillis(); @@ -164,13 +169,14 @@ public void calcAsas(int nSpherePoints, int nThreads, int cofactorSizeToUse) { String molecId1 = interf.getMoleculeIds().getFirst()+interf.getTransforms().getFirst().getTransformId(); String molecId2 = interf.getMoleculeIds().getSecond()+interf.getTransforms().getSecond().getTransformId(); + logger.debug("Calculating complexed ASAs for interface {} between molecules {} and {}", interf.getId(), molecId1, molecId2); + interf.setAsas(chainAsas.get(molecId1), chainAsas.get(molecId2), nSpherePoints, nThreads, cofactorSizeToUse); } end = System.currentTimeMillis(); - logger.debug("Calculated complexes ASA for "+list.size()+" pairwise complexes. " - + "Time: "+((end-start)/1000.0)+" s"); + logger.debug("Calculated complexes ASA for {} pairwise complexes. Time: {} s", list.size(), ((end-start)/1000.0)); // finally we sort based on the ChainInterface.comparable() (based in interfaceArea) 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 e7ea1d2481..981820dc08 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 @@ -20,22 +20,22 @@ */ package org.biojava.nbio.structure.asa; -import junit.framework.TestCase; -import org.biojava.nbio.structure.Structure; -import org.biojava.nbio.structure.StructureException; -import org.biojava.nbio.structure.StructureIO; +import org.biojava.nbio.structure.*; import org.biojava.nbio.structure.io.mmcif.ChemCompGroupFactory; import org.biojava.nbio.structure.io.mmcif.DownloadChemCompProvider; -import org.junit.Assert; +import static org.junit.Assert.*; + import org.junit.Test; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * Testing of Accessible Surface Area calculations * * - * @author duarte_j + * @author Jose Duarte * */ public class TestAsaCalc { @@ -70,12 +70,114 @@ public void testAsa3PIU() throws StructureException, IOException { //System.out.println(groupAsa.getGroup().getPDBName() + " " + groupAsa.getGroup().getResidueNumber() + " " + groupAsa.getAsaU()); totResidues+=groupAsa.getAsaU(); - Assert.assertTrue(groupAsa.getRelativeAsaU() <= 1.0); + assertTrue(groupAsa.getRelativeAsaU() <= 1.0); + } + + assertEquals(totAtoms, totResidues, 0.000001); + + assertEquals(17462.0, totAtoms, 1.0); + + } + + @Test + public void testNeighborIndicesFinding() throws StructureException, IOException { + // important: without this the tests can fail when running in maven (but not in IDE) + // that's because it depends on the order on how tests were run - JD 2018-03-10 + ChemCompGroupFactory.setChemCompProvider(new DownloadChemCompProvider()); + + Structure structure = StructureIO.getStructure("3PIU"); + + AsaCalculator asaCalc = new AsaCalculator(structure, + AsaCalculator.DEFAULT_PROBE_SIZE, + 1000, 1, false); + + int[][] allNbsSh = asaCalc.findNeighborIndicesSpatialHashing(); + + int[][] allNbs = asaCalc.findNeighborIndices(); + + for (int indexToTest =0; indexToTest < asaCalc.getAtomCoords().length; indexToTest++) { + //int indexToTest = 198; + int[] nbsSh = allNbsSh[indexToTest]; + int[] nbs = allNbs[indexToTest]; + + List listOfMatchingIndices = new ArrayList<>(); + for (int i = 0; i < nbsSh.length; i++) { + for (int j = 0; j < nbs.length; j++) { + if (nbs[j] == nbsSh[i]) { + listOfMatchingIndices.add(j); + break; + } + } + } + +// for (int i = 0; i