File size: 8,070 Bytes
10f2621 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | from Bio.PDB import *
import numpy as np
from sklearn.neighbors import KDTree
"""
Modified from:
computeCharges.py - MaSIF
Pablo Gainza - LPDI STI EPFL 2019
"""
"""
computeCharges.py: Wrapper function to compute hydrogen bond potential (free electrons/protons) in the surface
Pablo Gainza - LPDI STI EPFL 2019
This file is part of MaSIF.
Released under an Apache License 2.0
"""
from default_config.chemistry import (
polarHydrogens,
radii,
acceptorAngleAtom,
acceptorPlaneAtom,
hbond_std_dev,
donorAtom,
)
# Compute vertex charges based on hydrogen bond potential.
# pdb_filename: The filename of the protonated protein.
# vertices: The surface vertices of the protonated protein
# The name of each vertex in the format, example: B_125_x_ASN_ND2_Green
# where B is chain, 125 res id, x the insertion, ASN aatype, ND2 the name of the
# atom, and green is not used anymore.
def computeCharges(pdb_filename, vertices, names):
parser = PDBParser(QUIET=True)
struct = parser.get_structure(pdb_filename, pdb_filename + ".pdb")
residues = {}
for res in struct.get_residues():
chain_id = res.get_parent().get_id()
if chain_id == "":
chain_id = " "
residues[(chain_id, res.get_id())] = res
#atoms = Selection.unfold_entities(struct, "A")
atoms = struct.get_atoms()
satisfied_CO, satisfied_HN = computeSatisfied_CO_HN(atoms)
charge = np.array([0.0] * len(vertices))
# Go over every vertex
for ix, name in enumerate(names):
fields = name.split("_")
chain_id = fields[0]
if chain_id == "":
chain_id = " "
if fields[2] == "x":
fields[2] = " "
res_id = (" ", int(fields[1]), fields[2])
aa = fields[3]
atom_name = fields[4]
# Ignore atom if it is BB and it is already satisfied.
if atom_name == "H" and res_id in satisfied_HN:
continue
if atom_name == "O" and res_id in satisfied_CO:
continue
# Compute the charge of the vertex
charge[ix] = computeChargeHelper(
atom_name, residues[(chain_id, res_id)], vertices[ix]
)
return charge
# Compute the charge of a vertex in a residue.
def computeChargeHelper(atom_name, res, v):
res_type = res.get_resname()
# Check if it is a polar hydrogen.
if isPolarHydrogen(atom_name, res):
donor_atom_name = donorAtom[atom_name]
a = res[donor_atom_name].get_coord() # N/O
b = res[atom_name].get_coord() # H
# Donor-H is always 180.0 degrees, = pi
angle_deviation = computeAngleDeviation(a, b, v, np.pi)
angle_penalty = computeAnglePenalty(angle_deviation)
return 1.0 * angle_penalty
# Check if it is an acceptor oxygen or nitrogen
elif isAcceptorAtom(atom_name, res):
acceptor_atom = res[atom_name]
b = acceptor_atom.get_coord()
a = res[acceptorAngleAtom[atom_name]].get_coord()
# 120 degress for acceptor
angle_deviation = computeAngleDeviation(a, b, v, 2 * np.pi / 3)
# TODO: This should not be 120 for all atoms, i.e. for HIS it should be
# ~125.0
angle_penalty = computeAnglePenalty(angle_deviation)
plane_penalty = 1.0
if atom_name in acceptorPlaneAtom:
try:
d = res[acceptorPlaneAtom[atom_name]].get_coord()
except:
return 0.0
plane_deviation = computePlaneDeviation(d, a, b, v)
plane_penalty = computeAnglePenalty(plane_deviation)
return -1.0 * angle_penalty * plane_penalty
# Compute the
return 0.0
# Compute the absolute value of the deviation from theta
def computeAngleDeviation(a, b, c, theta):
return abs(calc_angle(Vector(a), Vector(b), Vector(c)) - theta)
# Compute the angle deviation from a plane
def computePlaneDeviation(a, b, c, d):
dih = calc_dihedral(Vector(a), Vector(b), Vector(c), Vector(d))
dev1 = abs(dih)
dev2 = np.pi - abs(dih)
return min(dev1, dev2)
# angle_deviation from ideal value. TODO: do a more data-based solution
def computeAnglePenalty(angle_deviation):
# Standard deviation: hbond_std_dev
return max(0.0, 1.0 - (angle_deviation / (hbond_std_dev)) ** 2)
def isPolarHydrogen(atom_name, res):
if atom_name in polarHydrogens[res.get_resname()]:
return True
else:
return False
def isAcceptorAtom(atom_name, res):
if atom_name.startswith("O"):
return True
else:
if res.get_resname() == "HIS":
if atom_name == "ND1" and "HD1" not in res:
return True
if atom_name == "NE2" and "HE2" not in res:
return True
return False
# Compute the list of backbone C=O:H-N that are satisfied. These will be ignored.
def computeSatisfied_CO_HN(atoms):
ns = NeighborSearch(atoms)
satisfied_CO = set()
satisfied_HN = set()
for atom1 in atoms:
res1 = atom1.get_parent()
if atom1.get_id() == "O":
neigh_atoms = ns.search(atom1.get_coord(), 2.5, level="A")
for atom2 in neigh_atoms:
if atom2.get_id() == "H":
res2 = atom2.get_parent()
# Ensure they belong to different residues.
if res2.get_id() != res1.get_id():
# Compute the angle N-H:O, ideal value is 180 (but in
# helices it is typically 160) 180 +-30 = pi
angle_N_H_O_dev = computeAngleDeviation(
res2["N"].get_coord(),
atom2.get_coord(),
atom1.get_coord(),
np.pi,
)
# Compute angle H:O=C, ideal value is ~160 +- 20 = 8*pi/9
angle_H_O_C_dev = computeAngleDeviation(
atom2.get_coord(),
atom1.get_coord(),
res1["C"].get_coord(),
8 * np.pi / 9,
)
## Allowed deviations: 30 degrees (pi/6) and 20 degrees
# (pi/9)
if (
angle_N_H_O_dev - np.pi / 6 < 0
and angle_H_O_C_dev - np.pi / 9 < 0.0
):
satisfied_CO.add(res1.get_id())
satisfied_HN.add(res2.get_id())
return satisfied_CO, satisfied_HN
# Compute the charge of a new mesh, based on the charge of an old mesh.
# Use the top vertex in distance, for now (later this should be smoothed over 3
# or 4 vertices)
def assignChargesToNewMesh(new_vertices, old_vertices, old_charges, seeder_opts):
dataset = old_vertices
testset = new_vertices
new_charges = np.zeros(len(new_vertices))
if seeder_opts["feature_interpolation"]:
num_inter = 4 # Number of interpolation features
# Assign k old vertices to each new vertex.
kdt = KDTree(dataset)
dists, result = kdt.query(testset, k=num_inter)
# Square the distances (as in the original pyflann)
dists = np.square(dists)
# The size of result is the same as new_vertices
for vi_new in range(len(result)):
vi_old = result[vi_new]
dist_old = dists[vi_new]
# If one vertex is right on top, ignore the rest.
if dist_old[0] == 0.0:
new_charges[vi_new] = old_charges[vi_old[0]]
continue
total_dist = np.sum(1 / dist_old)
for i in range(num_inter):
new_charges[vi_new] += (
old_charges[vi_old[i]] * (1 / dist_old[i]) / total_dist
)
else:
# Assign k old vertices to each new vertex.
kdt = KDTree(dataset)
dists, result = kdt.query(testset)
new_charges = old_charges[result]
return new_charges
|