Source code for propka.atom

"""
Atom
====

The :class:`Atom` class contains all atom information found in the PDB file.

"""

import string
from typing import List, Optional, TYPE_CHECKING

from propka.lib import make_tidy_atom_label
from . import hybrid36

if TYPE_CHECKING:
    from propka.group import Group
    from propka.molecular_container import MolecularContainer
    from propka.conformation_container import ConformationContainer

# Format strings that get used in multiple places (or are very complex)
PDB_LINE_FMT1 = (
    "{type:6s}{r.numb:>5d} {atom_label} {r.res_name}{r.chain_id:>2s}"
    "{r.res_num:>4d}{r.x:>12.3f}{r.y:>8.3f}{r.z:>8.3f}{r.occ:>6s}"
    "{r.beta:>6s}\n")
MOL2_LINE_FMT = (
    "{id:<4d} {atom_label:4s} "
    "{r.x:>10.4f} {r.y:>10.4f} {r.z:>10.4f} "
    "{r.sybyl_type:>6s} {r.res_num:>6d} {r.res_name:>10s}     0.0000\n")
PDB_LINE_FMT2 = (
    "ATOM {numb:>6d} {atom_label} {res_name}{chain_id:>2s}{res_num:>4d}"
    "{x:>12.3f}{y:>8.3f}{z:>8.3f}{occ:>6.2f}{beta:>6.2f}\n")
STR_FMT = (
    "{r.numb:>5d}-{r.name:>4s} {r.res_num:>5d}-{r.res_name:>3s} "
    "({r.chain_id:1s}) [{r.x:>8.3f} {r.y:>8.3f} {r.z:>8.3f}] {r.element:s}")


[docs] class Atom: """Atom class - contains all atom information found in the PDB file .. versionchanged:: 3.4.0 :meth:`make_input_line` and :meth:`get_input_parameters` have been removed as reading/writing PROPKA input is no longer supported. """ group: Optional["Group"] = None group_type: Optional[str] = None cysteine_bridge: bool = False conformation_container: Optional["ConformationContainer"] = None molecular_container: Optional["MolecularContainer"] = None is_protonated: bool = False steric_num_lone_pairs_set: bool = False terminal: Optional[str] = None charge: float = 0.0 charge_set: bool = False steric_number: int = 0 number_of_lone_pairs: int = 0 number_of_protons_to_add: int = 0 num_pi_elec_2_3_bonds: int = 0 num_pi_elec_conj_2_3_bonds: int = 0 groups_extracted: bool = False # PDB attributes name: str = '' numb: int = 0 x: float = 0.0 y: float = 0.0 z: float = 0.0 res_num: int = 0 res_name: str = '' chain_id: str = 'A' type: str = '' occ: str = '1.0' beta: str = '0.0' element: str = '' icode: str = '' # ligand atom types sybyl_type = '' sybyl_assigned = False marvin_pka = False def __init__(self, line: Optional[str] = None): """Initialize Atom object. Args: line: Line from a PDB file to set properties of atom. """ self.bonded_atoms: List[Atom] = [] self.set_properties(line) fmt = "{r.name:3s}{r.res_num:>4d}{r.chain_id:>2s}" self.residue_label = fmt.format(r=self)
[docs] def set_properties(self, line: Optional[str]): """Line from PDB file to set properties of atom. Args: line: PDB file line """ if line: self.name = line[12:16].strip() self.numb = int(hybrid36.decode(line[6:11])) self.x = float(line[30:38].strip()) self.y = float(line[38:46].strip()) self.z = float(line[46:54].strip()) self.res_num = int(line[22:26].strip()) self.res_name = "{0:<3s}".format(line[17:20].strip()) # Set chain id to "_" if it is just white space. self.chain_id = line[21].strip() or '_' self.type = line[:6].strip().lower() # TODO - define nucleic acid residue names elsewhere if self.res_name in ['DA ', 'DC ', 'DG ', 'DT ']: self.type = 'hetatm' self.occ = line[55:60].strip() self.beta = line[60:66].strip() self.icode = line[26:27] # Set the element using the position of the name in the pdb file self.element = line[12:14].strip().strip(string.digits) if len(self.name) == 4: self.element = self.element[0] if len(self.element) == 2: self.element = '{0:1s}{1:1s}'.format( self.element[0], self.element[1].lower())
[docs] def set_group_type(self, type_: str): """Set group type of atom. Args: type_: group type of atom """ self.group_type = type_
[docs] def count_bonded_elements(self, element): """Count number of bonded atoms with same element. Args: element: element type for test. Returns: number of bonded atoms. """ return len(self.get_bonded_elements(element))
[docs] def get_bonded_elements(self, element): """Get bonded atoms with same element. Args: element: element type for test. Returns: array of bonded atoms. """ res = [] for bond_atom in self.bonded_atoms: if bond_atom.element == element: res.append(bond_atom) return res
[docs] def get_bonded_heavy_atoms(self): """Get the atoms bonded to this one that aren't hydrogen. Returns: list of atoms. """ return [ba for ba in self.bonded_atoms if ba.element != 'H']
[docs] def is_atom_within_bond_distance(self, other_atom, max_bonds, cur_bond): """Check if <other_atom> is found within <max_bonds> bonds of self. Args: other_atom: atom to check max_bonds: number of bonds to check for other atom bonding to self Returns: Boolean for atom bond distance """ for ba in self.bonded_atoms: if ba == other_atom: return True if max_bonds > cur_bond: if ba.is_atom_within_bond_distance(other_atom, max_bonds, cur_bond+1): return True return False
[docs] def set_property(self, numb: Optional[int] = None, name: Optional[str] = None, res_name: Optional[str] = None, chain_id: Optional[str] = None, res_num: Optional[int] = None, x: Optional[float] = None, y: Optional[float] = None, z: Optional[float] = None, occ: Optional[str] = None, beta: Optional[str] = None): """Set properties of the atom object. Args: numb: Atom number name: Atom name res_name: residue name chain_id: chain ID res_num: residue number x: atom x-coordinate y: atom y-coordinate z: atom z-coordinate occ: atom occupancy beta: atom temperature factor """ if numb is not None: self.numb = numb if name is not None: self.name = name if res_name is not None: self.res_name = res_name if chain_id is not None: self.chain_id = chain_id if res_num is not None: self.res_num = res_num if x is not None: self.x = x if y is not None: self.y = y if z is not None: self.z = z if occ is not None: self.occ = occ if beta is not None: self.beta = beta
[docs] def make_copy(self): """Make a copy of this atom. Returns: Another atom object copy of this one. """ new_atom = Atom() new_atom.type = self.type new_atom.numb = self.numb new_atom.name = self.name new_atom.element = self.element new_atom.res_name = self.res_name new_atom.res_num = self.res_num new_atom.chain_id = self.chain_id new_atom.x = self.x new_atom.y = self.y new_atom.z = self.z new_atom.occ = self.occ new_atom.beta = self.beta new_atom.terminal = self.terminal new_atom.residue_label = self.residue_label new_atom.icode = self.icode return new_atom
[docs] def make_conect_line(self): """PDB line for bonding within this molecule. Returns: String with PDB line. """ res = 'CONECT{0:5d}'.format(self.numb) bonded = [] for atom in self.bonded_atoms: bonded.append(atom.numb) bonded.sort() for bond in bonded: res += '{0:5d}'.format(bond) res += '\n' return res
[docs] def make_pdb_line(self): """Create PDB line. TODO - this could/should be a @property method/attribute TODO - figure out difference between make_pdb_line, and make_pdb_line2 Returns: String with PDB line. """ str_ = PDB_LINE_FMT1.format( type=self.type.upper(), r=self, atom_label=make_tidy_atom_label(self.name, self.element)) return str_
[docs] def make_mol2_line(self, id_): """Create MOL2 line. Format: 1 S1 3.6147 2.0531 1.4795 S.3 1 noname -0.1785 TODO - this could/should be a @property method/attribute Returns: String with MOL2 line. """ str_ = MOL2_LINE_FMT.format( id=id_, r=self, atom_label=make_tidy_atom_label(self.name, self.element)) return str_
[docs] def get_tidy_label(self): """Returns a 'tidier' atom label for printing the new pdbfile TODO - this could/should be a @property method/attribute Returns: String with label""" return make_tidy_atom_label(self.name, self.element)
def __str__(self): """Return an undefined-format string version of this atom.""" return STR_FMT.format(r=self)