English
OneScience
protein structure generation
La-Proteina / models /utils /biopython_utils.py
anzhi2710gmailcom's picture
Upload folder using huggingface_hub
8e04e6f verified
Raw
History Blame Contribute Delete
9.04 kB
from collections import defaultdict
import numpy as np
from Bio.PDB import DSSP, PDBParser, Polypeptide, Selection, Superimposer
from Bio.PDB.Polypeptide import is_aa
from Bio.SeqUtils.ProtParam import ProteinAnalysis
from scipy.spatial import cKDTree
# analyze sequence composition of design
def validate_design_sequence(sequence, num_clashes, advanced_settings):
note_array = []
# Check if protein contains clashes after relaxation
if num_clashes > 0:
note_array.append("Relaxed structure contains clashes.")
# Check if the sequence contains disallowed amino acids
restricted_AAs = advanced_settings["omit_AAs"].split(",")
for restricted_AA in restricted_AAs:
if restricted_AA in sequence:
note_array.append("Contains: " + restricted_AA + "!")
# Analyze the protein
analysis = ProteinAnalysis(sequence)
# Calculate the reduced extinction coefficient per 1% solution
extinction_coefficient_reduced = analysis.molar_extinction_coefficient()[0]
molecular_weight = round(analysis.molecular_weight() / 1000, 2)
extinction_coefficient_reduced_1 = round(
extinction_coefficient_reduced / molecular_weight * 0.01, 2
)
# Check if the absorption is high enough
if extinction_coefficient_reduced_1 <= 2:
note_array.append(
f"Absorption value is {extinction_coefficient_reduced_1}, consider adding tryptophane to design."
)
# Join the notes into a single string
notes = " ".join(note_array)
return notes
# temporary function, calculate RMSD of input PDB and trajectory target
def target_pdb_rmsd(trajectory_pdb, starting_pdb, chain_ids_string):
# Parse the PDB files
parser = PDBParser(QUIET=True)
structure_trajectory = parser.get_structure("trajectory", trajectory_pdb)
structure_starting = parser.get_structure("starting", starting_pdb)
# Extract chain A from trajectory_pdb
chain_trajectory = structure_trajectory[0]["A"]
# Extract the specified chains from starting_pdb
chain_ids = chain_ids_string.split(",")
residues_starting = []
for chain_id in chain_ids:
chain_id = chain_id.strip()
chain = structure_starting[0][chain_id]
for residue in chain:
if is_aa(residue, standard=True):
residues_starting.append(residue)
# Extract residues from chain A in trajectory_pdb
residues_trajectory = [
residue for residue in chain_trajectory if is_aa(residue, standard=True)
]
# Ensure that both structures have the same number of residues
min_length = min(len(residues_starting), len(residues_trajectory))
residues_starting = residues_starting[:min_length]
residues_trajectory = residues_trajectory[:min_length]
# Collect CA atoms from the two sets of residues
atoms_starting = [residue["CA"] for residue in residues_starting if "CA" in residue]
atoms_trajectory = [
residue["CA"] for residue in residues_trajectory if "CA" in residue
]
# Calculate RMSD using structural alignment
sup = Superimposer()
sup.set_atoms(atoms_starting, atoms_trajectory)
rmsd = sup.rms
return round(rmsd, 2)
# detect C alpha clashes for deformed trajectories
def calculate_clash_score(pdb_file, threshold=2.4, only_ca=False):
parser = PDBParser(QUIET=True)
structure = parser.get_structure("protein", pdb_file)
atoms = []
atom_info = [] # Detailed atom info for debugging and processing
for model in structure:
for chain in model:
for residue in chain:
for atom in residue:
if atom.element == "H": # Skip hydrogen atoms
continue
if only_ca and atom.get_name() != "CA":
continue
atoms.append(atom.coord)
atom_info.append(
(chain.id, residue.id[1], atom.get_name(), atom.coord)
)
tree = cKDTree(atoms)
pairs = tree.query_pairs(threshold)
valid_pairs = set()
for i, j in pairs:
chain_i, res_i, name_i, coord_i = atom_info[i]
chain_j, res_j, name_j, coord_j = atom_info[j]
# Exclude clashes within the same residue
if chain_i == chain_j and res_i == res_j:
continue
# Exclude directly sequential residues in the same chain for all atoms
if chain_i == chain_j and abs(res_i - res_j) == 1:
continue
# If calculating sidechain clashes, only consider clashes between different chains
if not only_ca and chain_i == chain_j:
continue
valid_pairs.add((i, j))
return len(valid_pairs)
# identify interacting residues at the binder interface
def hotspot_residues(trajectory_pdb, binder_chain="B", atom_distance_cutoff=4.0):
# Parse the PDB file
parser = PDBParser(QUIET=True)
structure = parser.get_structure("complex", trajectory_pdb)
# Get the specified chain
binder_atoms = Selection.unfold_entities(structure[0][binder_chain], "A")
binder_coords = np.array([atom.coord for atom in binder_atoms])
# Get atoms and coords for the target chain
target_atoms = Selection.unfold_entities(structure[0]["A"], "A")
target_coords = np.array([atom.coord for atom in target_atoms])
# Build KD trees for both chains
binder_tree = cKDTree(binder_coords)
target_tree = cKDTree(target_coords)
# Prepare to collect interacting residues
interacting_residues = {}
# Query the tree for pairs of atoms within the distance cutoff
pairs = binder_tree.query_ball_tree(target_tree, atom_distance_cutoff)
# Process each binder atom's interactions
for binder_idx, close_indices in enumerate(pairs):
binder_residue = binder_atoms[binder_idx].get_parent()
binder_resname = binder_residue.get_resname()
if binder_resname in Polypeptide.standard_aa_names:
aa_single_letter = Polypeptide.three_to_one(binder_resname)
for close_idx in close_indices:
target_atoms[close_idx].get_parent()
interacting_residues[binder_residue.id[1]] = aa_single_letter
return interacting_residues
# calculate secondary structure percentage of design
def calc_ss_percentage(
pdb_file, advanced_settings, chain_id="B", atom_distance_cutoff=4.0
):
# Parse the structure
parser = PDBParser(QUIET=True)
structure = parser.get_structure("protein", pdb_file)
model = structure[0] # Consider only the first model in the structure
# Calculate DSSP for the model
dssp = DSSP(model, pdb_file, dssp=advanced_settings["dssp_path"])
# Prepare to count residues
ss_counts = defaultdict(int)
ss_interface_counts = defaultdict(int)
plddts_interface = []
plddts_ss = []
# Get chain and interacting residues once
chain = model[chain_id]
interacting_residues = set(
hotspot_residues(pdb_file, chain_id, atom_distance_cutoff).keys()
)
for residue in chain:
residue_id = residue.id[1]
if (chain_id, residue_id) in dssp:
ss = dssp[(chain_id, residue_id)][2] # Get the secondary structure
ss_type = "loop"
if ss in ["H", "G", "I"]:
ss_type = "helix"
elif ss == "E":
ss_type = "sheet"
ss_counts[ss_type] += 1
if ss_type != "loop":
# calculate secondary structure normalised pLDDT
avg_plddt_ss = sum(atom.bfactor for atom in residue) / len(residue)
plddts_ss.append(avg_plddt_ss)
if residue_id in interacting_residues:
ss_interface_counts[ss_type] += 1
# calculate interface pLDDT
avg_plddt_residue = sum(atom.bfactor for atom in residue) / len(residue)
plddts_interface.append(avg_plddt_residue)
# Calculate percentages
total_residues = sum(ss_counts.values())
total_interface_residues = sum(ss_interface_counts.values())
percentages = calculate_percentages(
total_residues, ss_counts["helix"], ss_counts["sheet"]
)
interface_percentages = calculate_percentages(
total_interface_residues,
ss_interface_counts["helix"],
ss_interface_counts["sheet"],
)
i_plddt = (
round(sum(plddts_interface) / len(plddts_interface) / 100, 2)
if plddts_interface
else 0
)
ss_plddt = round(sum(plddts_ss) / len(plddts_ss) / 100, 2) if plddts_ss else 0
return (*percentages, *interface_percentages, i_plddt, ss_plddt)
def calculate_percentages(total, helix, sheet):
helix_percentage = round((helix / total) * 100, 2) if total > 0 else 0
sheet_percentage = round((sheet / total) * 100, 2) if total > 0 else 0
loop_percentage = (
round(((total - helix - sheet) / total) * 100, 2) if total > 0 else 0
)
return helix_percentage, sheet_percentage, loop_percentage