#@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