Spaces:
Sleeping
Sleeping
#@title get secondary structure (SSE) from given PDB file | |
#@markdown So far it seems the best solution is to steal code from biotite | |
#@markdown which calculates the SSE of a peptide chain based on the P-SEA algorithm (Labesse 1997) | |
# CODE FROM BIOKITE | |
# From Krypton | |
import numpy as np | |
import random | |
import torch | |
def vector_dot(v1,v2): | |
return (v1*v2).sum(axis=-1) | |
def norm_vector(v): | |
factor = np.linalg.norm(v, axis=-1) | |
if isinstance(factor, np.ndarray): | |
v /= factor[..., np.newaxis] | |
else: | |
v /= factor | |
return v | |
def coord(x): | |
return np.asarray(x) | |
def displacement(atoms1, atoms2): | |
v1 = coord(atoms1) | |
v2 = coord(atoms2) | |
if len(v1.shape) <= len(v2.shape): | |
diff = v2 - v1 | |
else: | |
diff = -(v1 - v2) | |
return diff | |
def distance(atoms1, atoms2): | |
diff = displacement(atoms1, atoms2) | |
return np.sqrt(vector_dot(diff, diff)) | |
def angle(atoms1, atoms2, atoms3): | |
v1 = displacement(atoms1, atoms2) | |
v2 = displacement(atoms3, atoms2) | |
norm_vector(v1) | |
norm_vector(v2) | |
return np.arccos(vector_dot(v1,v2)) | |
def dihedral(atoms1, atoms2, atoms3, atoms4): | |
v1 = displacement(atoms1, atoms2) | |
v2 = displacement(atoms2, atoms3) | |
v3 = displacement(atoms3, atoms4) | |
norm_vector(v1) | |
norm_vector(v2) | |
norm_vector(v3) | |
n1 = np.cross(v1, v2) | |
n2 = np.cross(v2, v3) | |
# Calculation using atan2, to ensure the correct sign of the angle | |
x = vector_dot(n1,n2) | |
y = vector_dot(np.cross(n1,n2), v2) | |
return np.arctan2(y,x) | |
def replace_letters(arr): | |
# Create a dictionary that maps the letters 'a', 'b', and 'c' to the corresponding numbers | |
letter_to_number = {'a': 0, 'b': 1, 'c': 2} | |
# Create a new array that will hold the numbers | |
nums = [] | |
# Loop through the input array and replace the letters with the corresponding numbers | |
for letter in arr: | |
if letter in letter_to_number: | |
nums.append(letter_to_number[letter]) | |
else: | |
nums.append(letter) | |
return np.array(nums) | |
def replace_with_mask(arr, percentage, replace_loops=False): | |
# Make sure the percentage is between 0 and 100 | |
percentage = min(max(percentage, 0), 100) | |
# Calculate the number of values to replace | |
num_to_replace = int(len(arr) * percentage / 100) | |
# Choose a random subset of the array to replace | |
replace_indices = random.sample(range(len(arr)), num_to_replace) | |
# Replace the values at the chosen indices with the number 3 | |
for i in replace_indices: | |
arr[i] = 3 | |
if replace_loops: | |
for i in arr: | |
if arr[i] == 2: | |
arr[i] = 3 | |
return arr | |
def annotate_sse(ca_coord, percentage_mask=0, replace_loops=False): | |
_radians_to_angle = 2*np.pi/360 | |
_r_helix = ((89-12)*_radians_to_angle, (89+12)*_radians_to_angle) | |
_a_helix = ((50-20)*_radians_to_angle, (50+20)*_radians_to_angle) | |
_d2_helix = ((5.5-0.5), (5.5+0.5)) | |
_d3_helix = ((5.3-0.5), (5.3+0.5)) | |
_d4_helix = ((6.4-0.6), (6.4+0.6)) | |
_r_strand = ((124-14)*_radians_to_angle, (124+14)*_radians_to_angle) | |
_a_strand = ((-180)*_radians_to_angle, (-125)*_radians_to_angle, | |
(145)*_radians_to_angle, (180)*_radians_to_angle) | |
_d2_strand = ((6.7-0.6), (6.7+0.6)) | |
_d3_strand = ((9.9-0.9), (9.9+0.9)) | |
_d4_strand = ((12.4-1.1), (12.4+1.1)) | |
# Filter all CA atoms in the relevant chain. | |
d2i_coord = np.full(( len(ca_coord), 2, 3 ), np.nan) | |
d3i_coord = np.full(( len(ca_coord), 2, 3 ), np.nan) | |
d4i_coord = np.full(( len(ca_coord), 2, 3 ), np.nan) | |
ri_coord = np.full(( len(ca_coord), 3, 3 ), np.nan) | |
ai_coord = np.full(( len(ca_coord), 4, 3 ), np.nan) | |
# The distances and angles are not defined for the entire interval, | |
# therefore the indices do not have the full range | |
# Values that are not defined are NaN | |
for i in range(1, len(ca_coord)-1): | |
d2i_coord[i] = (ca_coord[i-1], ca_coord[i+1]) | |
for i in range(1, len(ca_coord)-2): | |
d3i_coord[i] = (ca_coord[i-1], ca_coord[i+2]) | |
for i in range(1, len(ca_coord)-3): | |
d4i_coord[i] = (ca_coord[i-1], ca_coord[i+3]) | |
for i in range(1, len(ca_coord)-1): | |
ri_coord[i] = (ca_coord[i-1], ca_coord[i], ca_coord[i+1]) | |
for i in range(1, len(ca_coord)-2): | |
ai_coord[i] = (ca_coord[i-1], ca_coord[i], | |
ca_coord[i+1], ca_coord[i+2]) | |
d2i = distance(d2i_coord[:,0], d2i_coord[:,1]) | |
d3i = distance(d3i_coord[:,0], d3i_coord[:,1]) | |
d4i = distance(d4i_coord[:,0], d4i_coord[:,1]) | |
ri = angle(ri_coord[:,0], ri_coord[:,1], ri_coord[:,2]) | |
ai = dihedral(ai_coord[:,0], ai_coord[:,1], | |
ai_coord[:,2], ai_coord[:,3]) | |
sse = np.full(len(ca_coord), "c", dtype="U1") | |
# Annotate helices | |
# Find CA that meet criteria for potential helices | |
is_pot_helix = np.zeros(len(sse), dtype=bool) | |
for i in range(len(sse)): | |
if ( | |
d3i[i] >= _d3_helix[0] and d3i[i] <= _d3_helix[1] | |
and d4i[i] >= _d4_helix[0] and d4i[i] <= _d4_helix[1] | |
) or ( | |
ri[i] >= _r_helix[0] and ri[i] <= _r_helix[1] | |
and ai[i] >= _a_helix[0] and ai[i] <= _a_helix[1] | |
): | |
is_pot_helix[i] = True | |
# Real helices are 5 consecutive helix elements | |
is_helix = np.zeros(len(sse), dtype=bool) | |
counter = 0 | |
for i in range(len(sse)): | |
if is_pot_helix[i]: | |
counter += 1 | |
else: | |
if counter >= 5: | |
is_helix[i-counter : i] = True | |
counter = 0 | |
# Extend the helices by one at each end if CA meets extension criteria | |
i = 0 | |
while i < len(sse): | |
if is_helix[i]: | |
sse[i] = "a" | |
if ( | |
d3i[i-1] >= _d3_helix[0] and d3i[i-1] <= _d3_helix[1] | |
) or ( | |
ri[i-1] >= _r_helix[0] and ri[i-1] <= _r_helix[1] | |
): | |
sse[i-1] = "a" | |
sse[i] = "a" | |
if ( | |
d3i[i+1] >= _d3_helix[0] and d3i[i+1] <= _d3_helix[1] | |
) or ( | |
ri[i+1] >= _r_helix[0] and ri[i+1] <= _r_helix[1] | |
): | |
sse[i+1] = "a" | |
i += 1 | |
# Annotate sheets | |
# Find CA that meet criteria for potential strands | |
is_pot_strand = np.zeros(len(sse), dtype=bool) | |
for i in range(len(sse)): | |
if ( d2i[i] >= _d2_strand[0] and d2i[i] <= _d2_strand[1] | |
and d3i[i] >= _d3_strand[0] and d3i[i] <= _d3_strand[1] | |
and d4i[i] >= _d4_strand[0] and d4i[i] <= _d4_strand[1] | |
) or ( | |
ri[i] >= _r_strand[0] and ri[i] <= _r_strand[1] | |
and ( (ai[i] >= _a_strand[0] and ai[i] <= _a_strand[1]) | |
or (ai[i] >= _a_strand[2] and ai[i] <= _a_strand[3])) | |
): | |
is_pot_strand[i] = True | |
# Real strands are 5 consecutive strand elements, | |
# or shorter fragments of at least 3 consecutive strand residues, | |
# if they are in hydrogen bond proximity to 5 other residues | |
pot_strand_coord = ca_coord[is_pot_strand] | |
is_strand = np.zeros(len(sse), dtype=bool) | |
counter = 0 | |
contacts = 0 | |
for i in range(len(sse)): | |
if is_pot_strand[i]: | |
counter += 1 | |
coord = ca_coord[i] | |
for strand_coord in ca_coord: | |
dist = distance(coord, strand_coord) | |
if dist >= 4.2 and dist <= 5.2: | |
contacts += 1 | |
else: | |
if counter >= 4: | |
is_strand[i-counter : i] = True | |
elif counter == 3 and contacts >= 5: | |
is_strand[i-counter : i] = True | |
counter = 0 | |
contacts = 0 | |
# Extend the strands by one at each end if CA meets extension criteria | |
i = 0 | |
while i < len(sse): | |
if is_strand[i]: | |
sse[i] = "b" | |
if d3i[i-1] >= _d3_strand[0] and d3i[i-1] <= _d3_strand[1]: | |
sse[i-1] = "b" | |
sse[i] = "b" | |
if d3i[i+1] >= _d3_strand[0] and d3i[i+1] <= _d3_strand[1]: | |
sse[i+1] = "b" | |
i += 1 | |
sse=replace_letters(sse) | |
sse=replace_with_mask(sse, percentage_mask, replace_loops=replace_loops) | |
sse=torch.nn.functional.one_hot(torch.tensor(sse), num_classes=4) | |
return sse | |