Implemented obtaining phi, psi, chi angles in main
This commit is contained in:
parent
24a55a56e0
commit
a21b8e97a7
0
src/backbone/__init__.py
Normal file
0
src/backbone/__init__.py
Normal file
75
src/backbone/ramachandran.py
Normal file
75
src/backbone/ramachandran.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
class RamachandranSampler:
|
||||||
|
def __init__(self):
|
||||||
|
# the weights tend towards the beta strand region to avoid collapse
|
||||||
|
|
||||||
|
# generalised for 18 amino acids
|
||||||
|
self.general_dist = [
|
||||||
|
# beta strand (top left)
|
||||||
|
{'phi_m': -120, 'phi_s': 20, 'psi_m': 140, 'psi_s': 20, 'w': 0.70},
|
||||||
|
# right handed helix (bottom left)
|
||||||
|
{'phi_m': -65, 'phi_s': 15, 'psi_m': -40, 'psi_s': 15, 'w': 0.25},
|
||||||
|
# left handed helix (top right)
|
||||||
|
{'phi_m': 60, 'phi_s': 15, 'psi_m': 40, 'psi_s': 15, 'w': 0.05}
|
||||||
|
]
|
||||||
|
|
||||||
|
# glycine more flexible
|
||||||
|
self.glycine_dist = [
|
||||||
|
# beta strand (top left)
|
||||||
|
{'phi_m': -100, 'phi_s': 30, 'psi_m': 140, 'psi_s': 30, 'w': 0.3},
|
||||||
|
# right handed helix (bottom left)
|
||||||
|
{'phi_m': -60, 'phi_s': 30, 'psi_m': -30, 'psi_s': 30, 'w': 0.2},
|
||||||
|
# left handed helix (top right)
|
||||||
|
{'phi_m': 60, 'phi_s': 30, 'psi_m': 30, 'psi_s': 30, 'w': 0.2},
|
||||||
|
# bottom right for glycine
|
||||||
|
{'phi_m': 100, 'phi_s': 30, 'psi_m': -140,'psi_s': 30, 'w': 0.3}
|
||||||
|
]
|
||||||
|
|
||||||
|
# proline no left handed helix
|
||||||
|
self.proline_dist = [
|
||||||
|
# beta strand (top left)
|
||||||
|
{'phi_m': -63, 'phi_s': 5, 'psi_m': 150, 'psi_s': 20, 'w': 0.8},
|
||||||
|
# right handed helix (bottom left)
|
||||||
|
{'phi_m': -63, 'phi_s': 5, 'psi_m': -35, 'psi_s': 20, 'w': 0.2}
|
||||||
|
]
|
||||||
|
|
||||||
|
# returns the correct distribution
|
||||||
|
def _get_distribution(self, res_name):
|
||||||
|
if res_name == 'GLY':
|
||||||
|
return self.glycine_dist
|
||||||
|
elif res_name == 'PRO':
|
||||||
|
return self.proline_dist
|
||||||
|
else:
|
||||||
|
return self.general_dist
|
||||||
|
|
||||||
|
# returns (phi, psi) for the residue (units degrees)
|
||||||
|
def sample(self, res_name):
|
||||||
|
# the possible regions to sample
|
||||||
|
dist_options = self._get_distribution(res_name)
|
||||||
|
|
||||||
|
weights = [d['w'] for d in dist_options]
|
||||||
|
# normalise the weights to ensure sum is exactly 1.0 (no float errors)
|
||||||
|
weights = np.array(weights) / np.sum(weights)
|
||||||
|
# choose a region based on the weights
|
||||||
|
choice_idx = np.random.choice(len(dist_options), p=weights)
|
||||||
|
selected = dist_options[choice_idx]
|
||||||
|
|
||||||
|
# do a gaussian sample to get some variation
|
||||||
|
phi = np.random.normal(selected['phi_m'], selected['phi_s'])
|
||||||
|
psi = np.random.normal(selected['psi_m'], selected['psi_s'])
|
||||||
|
|
||||||
|
# wrap angles to +-180 degrees
|
||||||
|
phi = (phi + 180) % 360 - 180
|
||||||
|
psi = (psi + 180) % 360 - 180
|
||||||
|
|
||||||
|
return phi, psi
|
||||||
|
|
||||||
|
# example
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sampler = RamachandranSampler()
|
||||||
|
|
||||||
|
print("Sampling 5 residues...")
|
||||||
|
for res in ['ALA', 'GLY', 'PRO', 'TRP', 'VAL']:
|
||||||
|
phi, psi = sampler.sample(res)
|
||||||
|
print(f"{res}: Phi={phi:.1f}, Psi={psi:.1f}")
|
||||||
|
|
@ -4,23 +4,20 @@ from mpl_toolkits.mplot3d import Axes3D
|
||||||
from nerf import place_atom_nerf
|
from nerf import place_atom_nerf
|
||||||
from ramachandran import RamachandranSampler
|
from ramachandran import RamachandranSampler
|
||||||
|
|
||||||
# --- Constants (Idealized Geometry in Angstroms & Degrees) ---
|
# Ideal geometry. Angstroms for lengths, degrees for angles.
|
||||||
GEO = {
|
GEO = {
|
||||||
'N_CA_len': 1.46,
|
'N_CA_len': 1.46,
|
||||||
'CA_C_len': 1.51,
|
'CA_C_len': 1.51,
|
||||||
'C_N_len': 1.33, # Peptide bond length
|
'C_N_len': 1.33,
|
||||||
|
'N_H_len': 1.01,
|
||||||
|
|
||||||
# --- MISSING KEYS ADDED HERE ---
|
|
||||||
'N_H_len': 1.01, # Backbone Amide H bond length
|
|
||||||
'C_N_H_angle': 119.0, # Angle for placing the H
|
|
||||||
# -------------------------------
|
|
||||||
|
|
||||||
# Bond Angles (Standard idealized values)
|
|
||||||
'N_CA_C_angle': 111.0,
|
'N_CA_C_angle': 111.0,
|
||||||
'CA_C_N_angle': 116.0,
|
'CA_C_N_angle': 116.0,
|
||||||
'C_N_CA_angle': 122.0,
|
'C_N_CA_angle': 122.0,
|
||||||
|
'C_N_H_angle': 119.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Legacy function checking if two atoms (implementation as CAs) are within threshold.
|
||||||
def check_clashes(new_coord, existing_coords, threshold=3.0):
|
def check_clashes(new_coord, existing_coords, threshold=3.0):
|
||||||
if len(existing_coords) == 0: return False
|
if len(existing_coords) == 0: return False
|
||||||
diff = existing_coords - new_coord
|
diff = existing_coords - new_coord
|
||||||
|
|
|
||||||
|
|
@ -2,45 +2,38 @@ import numpy as np
|
||||||
|
|
||||||
class RamachandranSampler:
|
class RamachandranSampler:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Format: 'Region_Name': {
|
|
||||||
# 'phi_mean': x, 'phi_sigma': x,
|
|
||||||
# 'psi_mean': x, 'psi_sigma': x,
|
|
||||||
# 'weight': probability
|
|
||||||
# }
|
|
||||||
|
|
||||||
# 1. GENERAL (18 Amino Acids)
|
# Generalised for 18 amino acids. Weighted for beta strand region
|
||||||
# Tuned for "Random Coil" (High Beta/PPII, Low Alpha)
|
|
||||||
self.general_dist = [
|
self.general_dist = [
|
||||||
# Beta / PPII Region (Favored in unfolded states)
|
# beta strand (top left)
|
||||||
{'phi_m': -120, 'phi_s': 20, 'psi_m': 140, 'psi_s': 20, 'w': 0.60},
|
{'phi_m': -120, 'phi_s': 20, 'psi_m': 140, 'psi_s': 20, 'w': 0.70},
|
||||||
# Alpha-Right Region (Less common in unfolded, but present)
|
# right handed helix (bottom left)
|
||||||
{'phi_m': -60, 'phi_s': 15, 'psi_m': -40, 'psi_s': 15, 'w': 0.30},
|
{'phi_m': -65, 'phi_s': 15, 'psi_m': -40, 'psi_s': 15, 'w': 0.25},
|
||||||
# Alpha-Left / Bridge (Rare)
|
# left handed helix (top right)
|
||||||
{'phi_m': 60, 'phi_s': 15, 'psi_m': 40, 'psi_s': 15, 'w': 0.10}
|
{'phi_m': 60, 'phi_s': 15, 'psi_m': 40, 'psi_s': 15, 'w': 0.05}
|
||||||
]
|
]
|
||||||
|
|
||||||
# 2. GLYCINE (Flexible)
|
# Glycine more flexible
|
||||||
# Glycine can visit valid regions in all 4 quadrants
|
|
||||||
self.glycine_dist = [
|
self.glycine_dist = [
|
||||||
# Top Left (Beta/PPII)
|
# beta strand (top left)
|
||||||
{'phi_m': -100, 'phi_s': 30, 'psi_m': 140, 'psi_s': 30, 'w': 0.3},
|
{'phi_m': -100, 'phi_s': 30, 'psi_m': 140, 'psi_s': 30, 'w': 0.3},
|
||||||
# Bottom Left (Alpha-R)
|
# right handed helix (bottom left)
|
||||||
{'phi_m': -60, 'phi_s': 30, 'psi_m': -30, 'psi_s': 30, 'w': 0.2},
|
{'phi_m': -60, 'phi_s': 30, 'psi_m': -30, 'psi_s': 30, 'w': 0.2},
|
||||||
# Top Right (Left-handed Helix - Unique to Gly)
|
# left handed helix (top right)
|
||||||
{'phi_m': 60, 'phi_s': 30, 'psi_m': 30, 'psi_s': 30, 'w': 0.2},
|
{'phi_m': 60, 'phi_s': 30, 'psi_m': 30, 'psi_s': 30, 'w': 0.2},
|
||||||
# Bottom Right (Inverted Beta)
|
# bottom right for glycine
|
||||||
{'phi_m': 100, 'phi_s': 30, 'psi_m': -140,'psi_s': 30, 'w': 0.3}
|
{'phi_m': 100, 'phi_s': 30, 'psi_m': -140,'psi_s': 30, 'w': 0.3}
|
||||||
]
|
]
|
||||||
|
|
||||||
# 3. PROLINE (Rigid)
|
# Proline no left handed helix
|
||||||
# Phi is strictly locked around -63 degrees
|
|
||||||
self.proline_dist = [
|
self.proline_dist = [
|
||||||
# Beta/PPII (Dominant)
|
# beta strand (top left)
|
||||||
{'phi_m': -63, 'phi_s': 5, 'psi_m': 150, 'psi_s': 20, 'w': 0.8},
|
{'phi_m': -63, 'phi_s': 5, 'psi_m': 150, 'psi_s': 20, 'w': 0.8},
|
||||||
# Alpha (Minor)
|
# right handed helix (bottom left)
|
||||||
{'phi_m': -63, 'phi_s': 5, 'psi_m': -35, 'psi_s': 20, 'w': 0.2}
|
{'phi_m': -63, 'phi_s': 5, 'psi_m': -35, 'psi_s': 20, 'w': 0.2}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# returns the correct distribution
|
||||||
def _get_distribution(self, res_name):
|
def _get_distribution(self, res_name):
|
||||||
if res_name == 'GLY':
|
if res_name == 'GLY':
|
||||||
return self.glycine_dist
|
return self.glycine_dist
|
||||||
|
|
@ -49,31 +42,29 @@ class RamachandranSampler:
|
||||||
else:
|
else:
|
||||||
return self.general_dist
|
return self.general_dist
|
||||||
|
|
||||||
|
# returns (phi, psi) for the residue (units degrees)
|
||||||
def sample(self, res_name):
|
def sample(self, res_name):
|
||||||
"""
|
# the possible regions to sample
|
||||||
Returns a tuple (phi, psi) in degrees for the given residue.
|
|
||||||
"""
|
|
||||||
dist_options = self._get_distribution(res_name)
|
dist_options = self._get_distribution(res_name)
|
||||||
|
|
||||||
# 1. Pick a region based on weights
|
|
||||||
weights = [d['w'] for d in dist_options]
|
weights = [d['w'] for d in dist_options]
|
||||||
# Normalize weights to ensure sum is 1.0 (to avoid float errors)
|
# normalise the weights to ensure sum is exactly 1.0 (no float errors)
|
||||||
weights = np.array(weights) / np.sum(weights)
|
weights = np.array(weights) / np.sum(weights)
|
||||||
|
# choose a region based on the weights
|
||||||
choice_idx = np.random.choice(len(dist_options), p=weights)
|
choice_idx = np.random.choice(len(dist_options), p=weights)
|
||||||
selected = dist_options[choice_idx]
|
selected = dist_options[choice_idx]
|
||||||
|
|
||||||
# 2. Sample Gaussian for that region
|
# do a gaussian sample to get some variation
|
||||||
phi = np.random.normal(selected['phi_m'], selected['phi_s'])
|
phi = np.random.normal(selected['phi_m'], selected['phi_s'])
|
||||||
psi = np.random.normal(selected['psi_m'], selected['psi_s'])
|
psi = np.random.normal(selected['psi_m'], selected['psi_s'])
|
||||||
|
|
||||||
# 3. Wrap angles to [-180, 180] range (optional but good practice)
|
# wrap angles to +-180 degrees
|
||||||
phi = (phi + 180) % 360 - 180
|
phi = (phi + 180) % 360 - 180
|
||||||
psi = (psi + 180) % 360 - 180
|
psi = (psi + 180) % 360 - 180
|
||||||
|
|
||||||
return phi, psi
|
return phi, psi
|
||||||
|
|
||||||
# --- Usage Example ---
|
# example
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sampler = RamachandranSampler()
|
sampler = RamachandranSampler()
|
||||||
|
|
||||||
|
|
|
||||||
15
src/main.py
Normal file
15
src/main.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
from backbone.ramachandran import RamachandranSampler
|
||||||
|
from rotamers.dunbrack import DunbrackRotamerLibrary
|
||||||
|
|
||||||
|
rs = RamachandranSampler()
|
||||||
|
rl = DunbrackRotamerLibrary()
|
||||||
|
|
||||||
|
res_name = "PHE"
|
||||||
|
|
||||||
|
print(f"Backbone torsion angles for {res_name}")
|
||||||
|
phi, psi = rs.sample(res_name)
|
||||||
|
print(f"phi: {phi}, psi: {psi}")
|
||||||
|
|
||||||
|
print(f"Sidechain rotamers for {res_name}")
|
||||||
|
for rotamer in rl.rotamer_params(res_name, 100, 100):
|
||||||
|
print(rotamer.p, rotamer.chis)
|
||||||
0
src/rotamers/__init__.py
Normal file
0
src/rotamers/__init__.py
Normal file
|
|
@ -25,7 +25,7 @@
|
||||||
_dependent_cache = {}
|
_dependent_cache = {}
|
||||||
_independent_cache = {}
|
_independent_cache = {}
|
||||||
|
|
||||||
from rotamers import RotamerLibrary, RotamerParams, UnsupportedResTypeError, NoResidueRotamersError
|
from rotamers.rotamers_lib import RotamerLibrary, RotamerParams, UnsupportedResTypeError, NoResidueRotamersError
|
||||||
|
|
||||||
class DunbrackRotamerLibrary(RotamerLibrary):
|
class DunbrackRotamerLibrary(RotamerLibrary):
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user