def volume_delaunay(model): """Returns dictionary containing volume for each residue in `model`. Parameters ---------- model: Bio.PDB.Model.Model Model of the protein structure. """ volume_dict = {} atoms = np.array([atom for atom in model.get_atoms()]) delaunay = Delaunay([atom.coord for atom in atoms]) for simplex in delaunay.simplices: parent_residues = Selection.get_unique_parents(atoms[simplex]) # Simplex is taken into account only if is totally contained in one # residue. if len(parent_residues) is 1: unique_parent = label_residue(parent_residues[0]) cv_simplex = ConvexHull([atom.coord for atom in atoms[simplex]]) volume_dict.setdefault(unique_parent, 0) volume_dict[unique_parent] += cv_simplex.volume return volume_dict
def void_ken_dill(model): """Return dict with the void of each residue in `model`. Atom radii are taken from [1] except for hidrogen taken from [2]. Parameters ---------- model: Bio.PDB.Model.Model The structure model of the protein Notes ----- [1]: D. Flatow et al. (Volumes and surface areas: Geometries and scaling relationships between coarse grained and atomic structures). [2]: J.C Gaines et al. (Packing in protein cores). """ atom_radii = { 'C': 1.70, 'F': 2.00, # Iron 'H': 1.02, 'I': 1.98, 'M': 2.00, # Magnesium 'N': 1.55, 'O': 1.52, 'S': 1.80 } atoms = np.array([atom for atom in model.get_atoms()]) delaunay_triangulation = Delaunay([atom.coord for atom in atoms]) simplices = delaunay_triangulation.simplices neighbors = delaunay_triangulation.neighbors def _empty_triangles(simplex): """Return True if simplex is empty, False otherwise. """ empty_triangles = [] for i in range(2): for j in range(i + 1, 3): for k in range(j + 1, 4): atom_i = atoms[simplex[i]] atom_j = atoms[simplex[j]] atom_k = atoms[simplex[k]] try: radius_i = atom_radii[atom_i.id[0]] except KeyError: raise Exception( "Radius of atom {} is not defined".format( atom_i.id)) try: radius_j = atom_radii[atom_j.id[0]] except KeyError: raise Exception( "Radius of atom {} is not defined".format( atom_j.id)) try: radius_k = atom_radii[atom_k.id[0]] except KeyError: raise Exception( "Radius of atom {} is not defined".format( atom_k.id)) if (radius_i + radius_j < atom_i - atom_j and radius_i + radius_k < atom_i - atom_k and radius_j + radius_k < atom_j - atom_k): empty_triangles.append((i, j, k)) # Simplex is open if for all 6 edges in the tetrahedron, the distance # between the endpoints is greater than the sum of their Van der Waals # radii. if len(empty_triangles) == 4: return True return False def _depth_first_search(index): """Return list of connected component and checked indices. """ checked_indices = [] stack = deque([index]) connected_component = [] if not _empty_triangles(simplices[index]): checked_indices.append(index) return connected_component, checked_indices while stack: checked_indices.append(index) local_neighbors = neighbors[index] # Check if residue at the boundary if -1 in local_neighbors: connected_component = [] return connected_component, checked_indices good_neighbors = [] for neighbor in local_neighbors: # Check if neighbor is an empty triangle and hasn't been # checked yet. if _empty_triangles(simplices[neighbor]): if neighbor not in checked_indices: good_neighbors.append(neighbor) if not good_neighbors: connected_component.append(index) # Remove `index` of stack as simplex has no `open` neighbors # left to check. stack.pop() # If still simplices in stack then set index to its last # simplex. if stack: index = stack[-1] else: stack.extend(good_neighbors) index = stack[-1] return connected_component, checked_indices def _bounded_regions(): """Return the bounded regions of the alpha-complex of the triangulation. """ indices = set(range(len(simplices))) checked_indices = [] connected_components = [] while indices - set(checked_indices): # Take an element at random from the different between indices and # checked indices. remaining_set = indices - set(checked_indices) # Pick one of the remaining set. index = next(iter(remaining_set)) connected_component, _checked_indices = _depth_first_search(index) if connected_component: connected_components.append(connected_component) checked_indices.extend(_checked_indices) return connected_components def _volume_overlap(conv_simplex, atoms_simplex): """Return the volume of the overlap between the sphere and the tetrahedron. The volume of the overlap between the tetrahedron and the sphere, V_{overlap} is given by the equation: V_{overlap} = frac{2*r^3}{6} * [ -pi + phi_1 + phi_2 + phi_3 ], where phi_i is the dihedral angle between the planes intersecting at edge `i` of the convex hull. If `n_1` and `n_2` are the normal vectors of the planes intersecting at `edge 1`, then phi_1 = - [ n_1 dot n_2 ]. Where `dot` represents the dot product of both vectors. Note: The minus sign in the formula is added taken into account the direction of the normal vectors found in the equation of the planes defined by the facets in `scipy.spatial.ConvexHull`. Parameters ---------- conv_simplex : scipy.spatial.ConvexHull Convex hull of a tetrahedron belonging to the Delaunay tesselation of a set of atomic coordinates. atoms_simplex : list Atoms used for the computation of the convexhull. The order of the `atoms_simplex` is the order of `conv_simplex.vertices`. """ # Equations of the planes defined by the facets of the convex hull are of # the form [a, b, c, d] where a*x + b*y + c*z + d = 0. Vector # n = (a, b, c) is the normal vector of the facet with direction to # the `outside` of the convex hull. equations = conv_simplex.equations # Tehtrahedra simplices = conv_simplex.simplices vertices = conv_simplex.vertices volume_overlaps = [] for vertex in vertices: dihedral_angles = [] # Adjacent facets of `vertex` adjacent_facets = np.where(simplices == vertex)[0] for first, second in [[0, 1], [1, 2], [2, 0]]: # Normal vectors normal_first = equations[adjacent_facets[first]][:-1] normal_second = equations[adjacent_facets[second]][:-1] # cos(phi) = - [ n_1 \cdot n_2 ] cosine_angle = -np.dot(normal_first, normal_second) dihedral_angle = np.arccos(cosine_angle) dihedral_angles.append(dihedral_angle) # Volume overlap # For `radius` take only the starting letter of atom. radius = atom_radii[atoms_simplex[vertex].id[0]] factor = (radius**3) / 3. volume_overlap = factor * (-np.pi + sum(dihedral_angles)) volume_overlaps.append(volume_overlap) return volume_overlaps connected_components = _bounded_regions() void = defaultdict(int) # In General expect nothing of this void. Residues are well-packed. inner_void = defaultdict(int) for connected_component in connected_components: for simplex in simplices[connected_component]: atoms_simplex = atoms[simplex] residues_simplex = Selection.get_unique_parents(atoms_simplex) conv_simplex = ConvexHull([atom.coord for atom in atoms_simplex]) volume_overlap = _volume_overlap(conv_simplex, atoms_simplex) simplex_void = conv_simplex.volume - sum(volume_overlap) if len(residues_simplex) > 1: for residue in residues_simplex: void[label_residue(residue)] += simplex_void else: residue = label_residue(residues_simplex[0]) inner_void[residue] += simplex_void return void, inner_void