from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from typing_extensions import Self
if TYPE_CHECKING:
from numpy.typing import NDArray
from deeprank2.molstruct.aminoacid import AminoAcid
from deeprank2.molstruct.atom import Atom
from deeprank2.molstruct.structure import Chain
from deeprank2.utils.pssmdata import PssmRow
[docs]class Residue:
"""One protein residue in a `PDBStructure`."""
def __init__(
self,
chain: Chain,
number: int,
amino_acid: AminoAcid | None = None,
insertion_code: str | None = None,
):
"""One protein residue in a `PDBStructure`.
A `Residue` is the basic building block of proteins and protein complex, here represented by `PDBStructures`.
Each `Residue` is of a certain `AminoAcid` type and consists of multiple `Atom`s.
Args:
chain: The chain that this residue belongs to.
number: the residue number
amino_acid: The residue's amino acid (if it's part of a protein). Defaults to None.
insertion_code: The pdb insertion code, if any. Defaults to None.
"""
self._chain = chain
self._number = number
self._amino_acid = amino_acid
self._insertion_code = insertion_code
self._atoms = []
def __eq__(self, other: Self) -> bool:
if isinstance(other, Residue):
return self._chain == other._chain and self._number == other._number and self._insertion_code == other._insertion_code
return NotImplemented
def __hash__(self) -> hash:
return hash((self._number, self._insertion_code))
[docs] def get_pssm(self) -> PssmRow:
"""Load pssm info linked to the residue."""
pssm = self._chain.pssm
if pssm is None:
msg = f"No pssm file found for Chain {self._chain}."
raise FileNotFoundError(msg)
return pssm[self]
@property
def number(self) -> int:
return self._number
@property
def chain(self) -> Chain:
return self._chain
@property
def amino_acid(self) -> AminoAcid:
return self._amino_acid
@property
def atoms(self) -> list[Atom]:
return self._atoms
@property
def number_string(self) -> str:
"""Contains both the number and the insertion code (if any)."""
if self._insertion_code is not None:
return f"{self._number}{self._insertion_code}"
return str(self._number)
@property
def insertion_code(self) -> str:
return self._insertion_code
[docs] def add_atom(self, atom: Atom) -> None:
self._atoms.append(atom)
def __repr__(self) -> str:
return f"{self._chain} {self.number_string}"
@property
def position(self) -> np.array:
return self.get_center()
[docs] def get_center(self) -> NDArray:
"""Find the center position of a `Residue`.
Center position is found as follows:
1. find beta carbon
2. if no beta carbon is found: find alpha carbon
3. if no alpha carbon is found: take the mean of the atom positions
"""
betas = [atom for atom in self.atoms if atom.name == "CB"]
if len(betas) > 0:
return betas[0].position
alphas = [atom for atom in self.atoms if atom.name == "CA"]
if len(alphas) > 0:
return alphas[0].position
if len(self.atoms) == 0:
msg = f"Cannot get the center position from {self}, because it has no atoms"
raise ValueError(msg)
return np.mean([atom.position for atom in self.atoms], axis=0)
[docs]class SingleResidueVariant:
"""A single residue mutation of a PDBStrcture.
Args:
residue: the `Residue` object from the PDBStructure that is mutated.
variant_amino_acid: the amino acid that the `Residue` is mutated into.
"""
def __init__(self, residue: Residue, variant_amino_acid: AminoAcid):
self._residue = residue
self._variant_amino_acid = variant_amino_acid
@property
def residue(self) -> Residue:
return self._residue
@property
def variant_amino_acid(self) -> AminoAcid:
return self._variant_amino_acid
@property
def wildtype_amino_acid(self) -> AminoAcid:
return self._residue.amino_acid