Implemented obtaining phi, psi, chi angles in main

This commit is contained in:
Joel Wallace 2025-11-15 21:27:36 +00:00
parent 24a55a56e0
commit a21b8e97a7
8 changed files with 121 additions and 43 deletions

0
src/backbone/__init__.py Normal file
View File

View 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}")

View File

@ -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
@ -165,4 +162,4 @@ if __name__ == "__main__":
backbone = generate_backbone_chain(test_seq, place_atom_nerf, sampler) backbone = generate_backbone_chain(test_seq, place_atom_nerf, sampler)
# 4. Visualize # 4. Visualize
plot_backbone(backbone) plot_backbone(backbone)

View File

@ -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
def sample(self, res_name): # returns (phi, psi) for the residue (units degrees)
""" def sample(self, res_name):
Returns a tuple (phi, psi) in degrees for the given residue. # the possible regions to sample
"""
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
View 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
View File

View 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):