diff --git a/plip/basic/config.py b/plip/basic/config.py index fdebdf4..a446e9e 100644 --- a/plip/basic/config.py +++ b/plip/basic/config.py @@ -1,79 +1,79 @@ -__version__ = '3.0.1' -__maintainer__ = 'PharmAI GmbH (2020-2021) - www.pharm.ai - hello@pharm.ai' -__citation_information__ = "Schake,P. Bolz,SN. et al. PLIP 2025: introducing protein–protein interactions to the protein–ligand interaction profiler. " \ +__version__: str = '3.0.1' +__maintainer__: str = 'PharmAI GmbH (2020-2021) - www.pharm.ai - hello@pharm.ai' +__citation_information__: str = "Schake,P. Bolz,SN. et al. PLIP 2025: introducing protein–protein interactions to the protein–ligand interaction profiler. " \ "Nucl. Acids Res. (10 May 2025), gkaf361. doi: 10.1093/nar/gkaf361" import logging -DEFAULT_LOG_LEVEL = logging.INFO -VERBOSE = False # Set verbose mode -QUIET = False # Set verbose mode -SILENT = False # Set verbose mode -MAXTHREADS = 1 # Maximum number of main threads for binding site visualization -XML = False -TXT = False -PICS = False -PYMOL = False -STDOUT = False -RAWSTRING = False # use raw strings for input / output -OUTPATH = './' -BASEPATH = './' -BREAKCOMPOSITE = False # Break up composite ligands with covalent bonds -ALTLOC = False # Consider alternate locations -PLUGIN_MODE = False # Special mode for PLIP in Plugins (e.g. PyMOL) -NOFIX = False # Turn off fixing of errors in PDB files -NOFIXFILE = False # Turn off writing to files for fixed PDB structures -PEPTIDES = [] # Definition which chains should be considered as peptide ligands -INTRA = None -RESIDUES = {} -KEEPMOD = False -DNARECEPTOR = False -OUTPUTFILENAME = None # Naming for the TXT and XML report files -NOPDBCANMAP = False # Skip calculation of mapping canonical atom order: PDB atom order -NOHYDRO = False # Do not add hydrogen bonds (in case already present in the structure) -MODEL = 1 # The model to be selected for multi-model structures (default = 1). -CHAINS = None # Define chains for protein-protein interaction detection -REGIONS = None -COMPRESS = False # Compress XML and TXT report files +DEFAULT_LOG_LEVEL: int = logging.INFO +VERBOSE: bool = False # Set verbose mode +QUIET: bool = False # Set verbose mode +SILENT: bool = False # Set verbose mode +MAXTHREADS: int = 1 # Maximum number of main threads for binding site visualization +XML: bool = False +TXT: bool = False +PICS: bool = False +PYMOL: bool = False +STDOUT: bool = False +RAWSTRING: bool = False # use raw strings for input / output +OUTPATH: str = './' +BASEPATH: str = './' +BREAKCOMPOSITE: bool = False # Break up composite ligands with covalent bonds +ALTLOC: bool = False # Consider alternate locations +PLUGIN_MODE: bool = False # Special mode for PLIP in Plugins (e.g. PyMOL) +NOFIX: bool = False # Turn off fixing of errors in PDB files +NOFIXFILE: bool = False # Turn off writing to files for fixed PDB structures +PEPTIDES: list[str] = [] # Definition which chains should be considered as peptide ligands +INTRA: str | None = None +RESIDUES: dict = {} +KEEPMOD: bool = False +DNARECEPTOR: bool = False +OUTPUTFILENAME: str | None = None # Naming for the TXT and XML report files +NOPDBCANMAP: bool = False # Skip calculation of mapping canonical atom order: PDB atom order +NOHYDRO: bool = False # Do not add hydrogen bonds (in case already present in the structure) +MODEL: int = 1 # The model to be selected for multi-model structures (default = 1). +CHAINS: list[list[str]] | None = None # Define chains for protein-protein interaction detection +REGIONS: list[tuple[dict[str, list[int]], dict[str, list[int]] | None]] | None = None +COMPRESS: bool = False # Compress XML and TXT report files # Configuration file for Protein-Ligand Interaction Profiler (PLIP) # Set thresholds for detection of interactions # Thresholds for detection (global variables) -BS_DIST = 7.5 # Determines maximum distance to include binding site residues -AROMATIC_PLANARITY = 5.0 # Determines allowed deviation from planarity in aromatic rings -MIN_DIST = 0.5 # Minimum distance for all distance thresholds +BS_DIST: float = 7.5 # Determines maximum distance to include binding site residues +AROMATIC_PLANARITY: float = 5.0 # Determines allowed deviation from planarity in aromatic rings +MIN_DIST: float = 0.5 # Minimum distance for all distance thresholds # Some distance thresholds were extended (max. 1.0A) if too restrictive too account for low-quality structures -HYDROPH_DIST_MAX = 4.0 # Distance cutoff for detection of hydrophobic contacts -HBOND_DIST_MAX = 4.1 # Max. distance between hydrogen bond donor and acceptor (Hubbard & Haider, 2001) + 0.6 A -HBOND_DON_ANGLE_MIN = 100 # Min. angle at the hydrogen bond donor (Hubbard & Haider, 2001) + 10 -PISTACK_DIST_MAX = 5.5 # Max. distance for parallel or offset pistacking (McGaughey, 1998) -PISTACK_ANG_DEV = 30 # Max. Deviation from parallel or perpendicular orientation (in degrees) -PISTACK_OFFSET_MAX = 2.0 # Maximum offset of the two rings (corresponds to the radius of benzene + 0.5 A) -PICATION_DIST_MAX = 6.0 # Max. distance between charged atom and aromatic ring center (Gallivan and Dougherty, 1999) -SALTBRIDGE_DIST_MAX = 5.5 # Max. distance between centers of charge for salt bridges (Barlow and Thornton, 1983) + 1.5 -HALOGEN_DIST_MAX = 4.0 # Max. distance between oxy. and halogen (Halogen bonds in biological molecules., Auffinger)+0.5 -HALOGEN_ACC_ANGLE = 120 # Optimal acceptor angle (Halogen bonds in biological molecules., Auffinger) -HALOGEN_DON_ANGLE = 165 # Optimal donor angle (Halogen bonds in biological molecules., Auffinger) -HALOGEN_ANGLE_DEV = 30 # Max. deviation from optimal angle -WATER_BRIDGE_MINDIST = 2.5 # Min. distance between water oxygen and polar atom (Jiang et al., 2005) -0.1 -WATER_BRIDGE_MAXDIST = 4.1 # Max. distance between water oxygen and polar atom (Jiang et al., 2005) +0.5 -WATER_BRIDGE_OMEGA_MIN = 71 # Min. angle between acceptor, water oxygen and donor hydrogen (Jiang et al., 2005) - 9 -WATER_BRIDGE_OMEGA_MAX = 140 # Max. angle between acceptor, water oxygen and donor hydrogen (Jiang et al., 2005) -WATER_BRIDGE_THETA_MIN = 100 # Min. angle between water oxygen, donor hydrogen and donor atom (Jiang et al., 2005) -METAL_DIST_MAX = 3.0 # Max. distance between metal ion and interacting atom (Harding, 2001) +HYDROPH_DIST_MAX: float = 4.0 # Distance cutoff for detection of hydrophobic contacts +HBOND_DIST_MAX: float = 4.1 # Max. distance between hydrogen bond donor and acceptor (Hubbard & Haider, 2001) + 0.6 A +HBOND_DON_ANGLE_MIN: float = 100 # Min. angle at the hydrogen bond donor (Hubbard & Haider, 2001) + 10 +PISTACK_DIST_MAX: float = 5.5 # Max. distance for parallel or offset pistacking (McGaughey, 1998) +PISTACK_ANG_DEV: float = 30 # Max. Deviation from parallel or perpendicular orientation (in degrees) +PISTACK_OFFSET_MAX: float = 2.0 # Maximum offset of the two rings (corresponds to the radius of benzene + 0.5 A) +PICATION_DIST_MAX: float = 6.0 # Max. distance between charged atom and aromatic ring center (Gallivan and Dougherty, 1999) +SALTBRIDGE_DIST_MAX: float = 5.5 # Max. distance between centers of charge for salt bridges (Barlow and Thornton, 1983) + 1.5 +HALOGEN_DIST_MAX: float = 4.0 # Max. distance between oxy. and halogen (Halogen bonds in biological molecules., Auffinger)+0.5 +HALOGEN_ACC_ANGLE: float = 120 # Optimal acceptor angle (Halogen bonds in biological molecules., Auffinger) +HALOGEN_DON_ANGLE: float = 165 # Optimal donor angle (Halogen bonds in biological molecules., Auffinger) +HALOGEN_ANGLE_DEV: float = 30 # Max. deviation from optimal angle +WATER_BRIDGE_MINDIST: float = 2.5 # Min. distance between water oxygen and polar atom (Jiang et al., 2005) -0.1 +WATER_BRIDGE_MAXDIST: float = 4.1 # Max. distance between water oxygen and polar atom (Jiang et al., 2005) +0.5 +WATER_BRIDGE_OMEGA_MIN: float = 71 # Min. angle between acceptor, water oxygen and donor hydrogen (Jiang et al., 2005) - 9 +WATER_BRIDGE_OMEGA_MAX: float = 140 # Max. angle between acceptor, water oxygen and donor hydrogen (Jiang et al., 2005) +WATER_BRIDGE_THETA_MIN: float = 100 # Min. angle between water oxygen, donor hydrogen and donor atom (Jiang et al., 2005) +METAL_DIST_MAX: float = 3.0 # Max. distance between metal ion and interacting atom (Harding, 2001) # Other thresholds -MAX_COMPOSITE_LENGTH = 200 # Filter out ligands with more than 200 fragments +MAX_COMPOSITE_LENGTH: int = 200 # Filter out ligands with more than 200 fragments ######### # Names # ######### # Names of RNA and DNA residues to be considered (detection by name) -RNA = ['U', 'A', 'C', 'G'] -DNA = ['DT', 'DA', 'DC', 'DG'] +RNA: list[str] = ['U', 'A', 'C', 'G'] +DNA: list[str] = ['DT', 'DA', 'DC', 'DG'] ############# # Whitelist # @@ -81,7 +81,7 @@ # Metal cations which can be complexed -METAL_IONS = ['CA', 'CO', 'MG', 'MN', 'FE', 'CU', 'ZN', 'FE2', 'FE3', 'FE4', 'LI', 'NA', 'K', 'RB', 'SR', 'CS', 'BA', +METAL_IONS: list[str] = ['CA', 'CO', 'MG', 'MN', 'FE', 'CU', 'ZN', 'FE2', 'FE3', 'FE4', 'LI', 'NA', 'K', 'RB', 'SR', 'CS', 'BA', 'CR', 'NI', 'FE1', 'NI', 'RU', 'RU1', 'RH', 'RH1', 'PD', 'AG', 'CD', 'LA', 'W', 'W1', 'OS', 'IR', 'PT', 'PT1', 'AU', 'HG', 'CE', 'PR', 'SM', 'EU', 'GD', 'TB', 'YB', 'LU', 'AL', 'GA', 'IN', 'SB', 'TL', 'PB'] @@ -90,13 +90,13 @@ ############## # Other Ions/Atoms (not yet supported) -anions = ['CL', 'IOD', 'BR'] -other = ['MO', 'RE', 'HO'] -UNSUPPORTED = anions + other +anions: list[str] = ['CL', 'IOD', 'BR'] +other: list[str] = ['MO', 'RE', 'HO'] +UNSUPPORTED: list[str] = anions + other # BioLiP list of suspicious ligands from http://zhanglab.ccmb.med.umich.edu/BioLiP/ligand_list (2014-07-10) # Add ligands here to get warnings for possible artifacts. -biolip_list = ['ACE', 'HEX', 'TMA', 'SOH', 'P25', 'CCN', 'PR', 'PTN', 'NO3', 'TCN', 'BU1', 'BCN', 'CB3', 'HCS', 'NBN', +biolip_list: list[str] = ['ACE', 'HEX', 'TMA', 'SOH', 'P25', 'CCN', 'PR', 'PTN', 'NO3', 'TCN', 'BU1', 'BCN', 'CB3', 'HCS', 'NBN', 'SO2', 'MO6', 'MOH', 'CAC', 'MLT', 'KR', '6PH', 'MOS', 'UNL', 'MO3', 'SR', 'CD3', 'PB', 'ACM', 'LUT', 'PMS', 'OF3', 'SCN', 'DHB', 'E4N', '13P', '3PG', 'CYC', 'NC', 'BEN', 'NAO', 'PHQ', 'EPE', 'BME', 'TB', 'ETE', 'EU', 'OES', 'EAP', 'ETX', 'BEZ', '5AD', 'OC2', 'OLA', 'GD3', 'CIT', 'DVT', 'OC6', 'MW1', 'OC3', diff --git a/plip/basic/logger.py b/plip/basic/logger.py index ca65411..b30c982 100644 --- a/plip/basic/logger.py +++ b/plip/basic/logger.py @@ -2,7 +2,7 @@ import logging -def get_logger(): +def get_logger() -> logging.Logger: """ Configures a base logger and returns a module-specific sub-logger of the calling module. """ diff --git a/plip/basic/parallel.py b/plip/basic/parallel.py index 1f71943..4b1ff9e 100644 --- a/plip/basic/parallel.py +++ b/plip/basic/parallel.py @@ -1,20 +1,25 @@ import itertools import multiprocessing from builtins import zip +from collections.abc import Callable, Iterable, Iterator from functools import partial +from typing import Any, TypeVar from numpy import asarray +T = TypeVar("T") +R = TypeVar("R") + class SubProcessError(Exception): - def __init__(self, e, exitcode=1): + def __init__(self, e: Exception, exitcode: int = 1) -> None: self.exitcode = exitcode super(SubProcessError, self).__init__(e) pass -def universal_worker(input_pair): +def universal_worker(input_pair: tuple[Callable[..., R], T, dict[str, Any]]) -> R: """This is a wrapper function expecting a tiplet of function, single argument, dict of keyword arguments. The provided function is called with the appropriate arguments.""" @@ -22,17 +27,25 @@ def universal_worker(input_pair): return function(arg, **kwargs) -def pool_args(function, sequence, kwargs): +def pool_args( + function: Callable[..., R], + sequence: Iterable[T], + kwargs: dict[str, Any], +) -> Iterator[tuple[Callable[..., R], T, dict[str, Any]]]: """Return a single iterator of n elements of lists of length 3, given a sequence of len n.""" return zip(itertools.repeat(function), sequence, itertools.repeat(kwargs)) -def parallel_fn(f): +def parallel_fn(f: Callable[..., R]) -> Callable[..., object]: """Simple wrapper function, returning a parallel version of the given function f. The function f must have one argument and may have an arbitray number of keyword arguments. """ - def simple_parallel(func, sequence, **args): + def simple_parallel( + func: Callable[..., R], + sequence: Iterable[T], + **args: Any, + ) -> object: """ f takes an element of sequence as input and the keyword args in **args""" if 'processes' in args: processes = args.get('processes') diff --git a/plip/basic/remote.py b/plip/basic/remote.py index 89af974..f0c1183 100644 --- a/plip/basic/remote.py +++ b/plip/basic/remote.py @@ -1,20 +1,81 @@ -from collections import namedtuple +from typing import NamedTuple -hbonds_info = namedtuple('hbonds_info', 'ldon_id lig_don_id prot_acc_id pdon_id prot_don_id lig_acc_id') -hydrophobic_info = namedtuple('hydrophobic_info', 'bs_ids lig_ids pairs_ids') -halogen_info = namedtuple('halogen_info', 'don_id acc_id') -pistack_info = namedtuple('pistack_info', 'proteinring_atoms, proteinring_center ligandring_atoms ' - 'ligandring_center type') -pication_info = namedtuple('pication_info', 'ring_center charge_center ring_atoms charge_atoms, protcharged') -sbridge_info = namedtuple('sbridge_info', 'positive_atoms negative_atoms positive_center negative_center protispos') -wbridge_info = namedtuple('wbridge_info', 'don_id acc_id water_id protisdon') -metal_info = namedtuple('metal_info', 'metal_id, target_id location') +from plip.structure.preparation import PDBComplex +from plip.structure.records import Coordinate + + +class HydrogenBondVisualization(NamedTuple): + ldon_id: list[tuple[int, int]] + lig_don_id: list[int] + prot_acc_id: list[int] + pdon_id: list[tuple[int, int]] + prot_don_id: list[int] + lig_acc_id: list[int] + + +class HydrophobicVisualization(NamedTuple): + bs_ids: list[int] + lig_ids: list[int] + pairs_ids: list[tuple[int, int]] + + +class HalogenVisualization(NamedTuple): + don_id: int + acc_id: int + + +class PiStackVisualization(NamedTuple): + proteinring_atoms: list[int] + proteinring_center: Coordinate + ligandring_atoms: list[int] + ligandring_center: Coordinate + type: str + + +class PiCationVisualization(NamedTuple): + ring_center: Coordinate + charge_center: Coordinate + ring_atoms: list[int] + charge_atoms: list[int] + protcharged: bool + + +class SaltBridgeVisualization(NamedTuple): + positive_atoms: list[int] + negative_atoms: list[int] + positive_center: Coordinate + negative_center: Coordinate + protispos: bool + + +class WaterBridgeVisualization(NamedTuple): + don_id: int + acc_id: int + water_id: int + protisdon: bool + + +class MetalVisualization(NamedTuple): + metal_id: int + target_id: int + location: str + + +# Backwards-compatible aliases for the historical public record names. +hbonds_info = HydrogenBondVisualization +hydrophobic_info = HydrophobicVisualization +halogen_info = HalogenVisualization +pistack_info = PiStackVisualization +pication_info = PiCationVisualization +sbridge_info = SaltBridgeVisualization +wbridge_info = WaterBridgeVisualization +metal_info = MetalVisualization class VisualizerData: """Contains all information on a complex relevant for visualization. Can be pickled""" - def __init__(self, mol, site): + def __init__(self, mol: PDBComplex, site: str) -> None: pcomp = mol pli = mol.interaction_sets[site] ligand = pli.ligand @@ -41,56 +102,60 @@ def __init__(self, mol, site): # Hydrophobic Contacts # Contains IDs of contributing binding site, ligand atoms and the pairings hydroph_pairs_id = [(h.bsatom_orig_idx, h.ligatom_orig_idx) for h in pli.hydrophobic_contacts] - self.hydrophobic_contacts = hydrophobic_info(bs_ids=[hp[0] for hp in hydroph_pairs_id], - lig_ids=[hp[1] for hp in hydroph_pairs_id], - pairs_ids=hydroph_pairs_id) + self.hydrophobic_contacts = HydrophobicVisualization(bs_ids=[hp[0] for hp in hydroph_pairs_id], + lig_ids=[hp[1] for hp in hydroph_pairs_id], + pairs_ids=hydroph_pairs_id) # Hydrogen Bonds # #@todo Don't use indices, simplify this code here hbonds_ldon, hbonds_pdon = pli.hbonds_ldon, pli.hbonds_pdon hbonds_ldon_id = [(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_ldon] hbonds_pdon_id = [(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_pdon] - self.hbonds = hbonds_info(ldon_id=[(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_ldon], - lig_don_id=[hb[1] for hb in hbonds_ldon_id], - prot_acc_id=[hb[0] for hb in hbonds_ldon_id], - pdon_id=[(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_pdon], - prot_don_id=[hb[1] for hb in hbonds_pdon_id], - lig_acc_id=[hb[0] for hb in hbonds_pdon_id]) + self.hbonds = HydrogenBondVisualization( + ldon_id=[(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_ldon], + lig_don_id=[hb[1] for hb in hbonds_ldon_id], + prot_acc_id=[hb[0] for hb in hbonds_ldon_id], + pdon_id=[(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_pdon], + prot_don_id=[hb[1] for hb in hbonds_pdon_id], + lig_acc_id=[hb[0] for hb in hbonds_pdon_id], + ) # Halogen Bonds - self.halogen_bonds = [halogen_info(don_id=h.don_orig_idx, acc_id=h.acc_orig_idx) + self.halogen_bonds = [HalogenVisualization(don_id=h.don_orig_idx, acc_id=h.acc_orig_idx) for h in pli.halogen_bonds] # Pistacking - self.pistacking = [pistack_info(proteinring_atoms=pistack.proteinring.atoms_orig_idx, - proteinring_center=pistack.proteinring.center, - ligandring_atoms=pistack.ligandring.atoms_orig_idx, - ligandring_center=pistack.ligandring.center, - type=pistack.type) for pistack in pli.pistacking] + self.pistacking = [PiStackVisualization(proteinring_atoms=pistack.proteinring.atoms_orig_idx, + proteinring_center=pistack.proteinring.center, + ligandring_atoms=pistack.ligandring.atoms_orig_idx, + ligandring_center=pistack.ligandring.center, + type=pistack.type) for pistack in pli.pistacking] # Pi-cation interactions - self.pication = [pication_info(ring_center=picat.ring.center, - charge_center=picat.charge.center, - ring_atoms=picat.ring.atoms_orig_idx, - charge_atoms=picat.charge.atoms_orig_idx, - protcharged=picat.protcharged) + self.pication = [PiCationVisualization(ring_center=picat.ring.center, + charge_center=picat.charge.center, + ring_atoms=picat.ring.atoms_orig_idx, + charge_atoms=picat.charge.atoms_orig_idx, + protcharged=picat.protcharged) for picat in pli.pication_paro + pli.pication_laro] # Salt Bridges - self.saltbridges = [sbridge_info(positive_atoms=sbridge.positive.atoms_orig_idx, - negative_atoms=sbridge.negative.atoms_orig_idx, - positive_center=sbridge.positive.center, - negative_center=sbridge.negative.center, - protispos=sbridge.protispos) + self.saltbridges = [SaltBridgeVisualization(positive_atoms=sbridge.positive.atoms_orig_idx, + negative_atoms=sbridge.negative.atoms_orig_idx, + positive_center=sbridge.positive.center, + negative_center=sbridge.negative.center, + protispos=sbridge.protispos) for sbridge in pli.saltbridge_lneg + pli.saltbridge_pneg] # Water Bridgese('wbridge_info', 'don_id acc_id water_id protisdon') - self.waterbridges = [wbridge_info(don_id=wbridge.d_orig_idx, - acc_id=wbridge.a_orig_idx, - water_id=wbridge.water_orig_idx, - protisdon=wbridge.protisdon) for wbridge in pli.water_bridges] + self.waterbridges = [WaterBridgeVisualization(don_id=wbridge.d_orig_idx, + acc_id=wbridge.a_orig_idx, + water_id=wbridge.water_orig_idx, + protisdon=wbridge.protisdon) + for wbridge in pli.water_bridges] # Metal Complexes - self.metal_complexes = [metal_info(metal_id=metalc.metal_orig_idx, - target_id=metalc.target_orig_idx, - location=metalc.location) for metalc in pli.metal_complexes] + self.metal_complexes = [MetalVisualization(metal_id=metalc.metal_orig_idx, + target_id=metalc.target_orig_idx, + location=metalc.location) + for metalc in pli.metal_complexes] diff --git a/plip/basic/supplemental.py b/plip/basic/supplemental.py index 2f4b990..d4764f4 100644 --- a/plip/basic/supplemental.py +++ b/plip/basic/supplemental.py @@ -6,28 +6,37 @@ import sys import tempfile import zipfile -from collections import namedtuple +from collections.abc import Iterable, Iterator, Sequence +from typing import IO, TypeAlias import numpy as np from openbabel import pybel +from openbabel.openbabel import OBAtom, OBResidue, OBRing from openbabel.pybel import Atom from plip.basic import config, logger +from plip.structure.records import CovalentLink logger = logger.get_logger() -def tmpfile(prefix, direc): +Coordinate: TypeAlias = Sequence[float] +Region: TypeAlias = dict[str, list[int]] +RegionPair: TypeAlias = tuple[Region, Region | None] +LigandMember: TypeAlias = tuple[str, str, int] + + +def tmpfile(prefix: str, direc: str) -> str: """Returns the path to a newly created temporary file.""" return tempfile.mktemp(prefix=prefix, suffix='.pdb', dir=direc) -def is_lig(hetid): +def is_lig(hetid: str) -> bool: """Checks if a PDB compound can be excluded as a small molecule ligand""" h = hetid.upper() return not (h == 'HOH' or h in config.UNSUPPORTED) -def extract_pdbid(string): +def extract_pdbid(string: str) -> str: """Use regular expressions to get a PDB ID from a string""" p = re.compile("[0-9][0-9a-z]{3}") m = p.search(string.lower()) @@ -37,25 +46,25 @@ def extract_pdbid(string): return "UnknownProtein" -def whichrestype(atom): +def whichrestype(atom: Atom | OBAtom) -> str | None: """Returns the residue name of an Pybel or OpenBabel atom.""" atom = atom if not isinstance(atom, Atom) else atom.OBAtom # Convert to OpenBabel Atom return atom.GetResidue().GetName() if atom.GetResidue() is not None else None -def whichresnumber(atom): +def whichresnumber(atom: Atom | OBAtom) -> int | None: """Returns the residue number of an Pybel or OpenBabel atom (numbering as in original PDB file).""" atom = atom if not isinstance(atom, Atom) else atom.OBAtom # Convert to OpenBabel Atom return atom.GetResidue().GetNum() if atom.GetResidue() is not None else None -def whichchain(atom): +def whichchain(atom: Atom | OBAtom) -> str | None: """Returns the residue number of an PyBel or OpenBabel atom.""" atom = atom if not isinstance(atom, Atom) else atom.OBAtom # Convert to OpenBabel Atom return atom.GetResidue().GetChain() if atom.GetResidue() is not None else None -def residue_belongs_to_receptor(res, regions=None): +def residue_belongs_to_receptor(res: OBResidue, regions: RegionPair | None = None) -> bool: """tests whether the residue is defined as receptor and is not part of a peptide or residue ligand.""" if regions: ligand_region, bs_region = regions @@ -86,14 +95,14 @@ def residue_belongs_to_receptor(res, regions=None): ######################### -def euclidean3d(v1, v2): +def euclidean3d(v1: Coordinate, v2: Coordinate) -> float | None: """Faster implementation of euclidean distance for the 3D case.""" if not len(v1) == 3 and len(v2) == 3: return None return np.sqrt((v1[0] - v2[0]) ** 2 + (v1[1] - v2[1]) ** 2 + (v1[2] - v2[2]) ** 2) -def vector(p1, p2): +def vector(p1: Coordinate, p2: Coordinate) -> np.ndarray | None: """Vector from p1 to p2. :param p1: coordinates of point p1 :param p2: coordinates of point p2 @@ -102,7 +111,7 @@ def vector(p1, p2): return None if len(p1) != len(p2) else np.array([p2[i] - p1[i] for i in range(len(p1))]) -def vecangle(v1, v2, deg=True): +def vecangle(v1: Coordinate, v2: Coordinate, deg: bool = True) -> float: """Calculate the angle between two vectors :param v1: coordinates of vector v1 :param v2: coordinates of vector v2 @@ -117,7 +126,7 @@ def vecangle(v1, v2, deg=True): return np.degrees([angle, ])[0] if deg else angle -def normalize_vector(v): +def normalize_vector(v: np.ndarray) -> np.ndarray: """Take a vector and return the normalized vector :param v: a vector v :returns : normalized vector v @@ -126,7 +135,7 @@ def normalize_vector(v): return v / norm if not norm == 0 else v -def centroid(coo): +def centroid(coo: Sequence[Coordinate]) -> list[float]: """Calculates the centroid from a 3D point cloud and returns the coordinates :param coo: Array of coordinate arrays :returns : centroid coordinates as list @@ -134,7 +143,7 @@ def centroid(coo): return list(map(np.mean, (([c[0] for c in coo]), ([c[1] for c in coo]), ([c[2] for c in coo])))) -def projection(pnormal1, ppoint, tpoint): +def projection(pnormal1: Coordinate, ppoint: Coordinate, tpoint: Coordinate) -> list[float]: """Calculates the centroid from a 3D point cloud and returns the coordinates :param pnormal1: normal of plane :param ppoint: coordinates of point in the plane @@ -153,7 +162,7 @@ def projection(pnormal1, ppoint, tpoint): return [c1 + c2 for c1, c2 in zip(tpoint, [sb * pn for pn in pnormal])] -def cluster_doubles(double_list): +def cluster_doubles(double_list: Sequence[tuple]) -> Iterator[tuple]: """Given a list of doubles, they are clustered if they share one element :param double_list: list of doubles :returns : list of clusters (tuples) @@ -198,7 +207,7 @@ def cluster_doubles(double_list): # File operations ################# -def tilde_expansion(folder_paths): +def tilde_expansion(folder_paths: str | list[str]) -> str | list[str]: """Tilde expansion, i.e. converts '~' in paths into .""" if isinstance(folder_paths, list): expanded_paths = [] @@ -211,12 +220,12 @@ def tilde_expansion(folder_paths): return os.path.expanduser(folder_paths) if "~" in folder_paths else folder_paths -def folder_exists(folder_path): +def folder_exists(folder_path: str) -> bool: """Checks if a folder exists""" return os.path.exists(folder_path) -def create_folder_if_not_exists(folder_path): +def create_folder_if_not_exists(folder_path: str) -> None: """Creates a folder if it does not exists.""" folder_path = tilde_expansion(folder_path) folder_path = "".join([folder_path, '/']) if not folder_path[-1] == '/' else folder_path @@ -225,7 +234,7 @@ def create_folder_if_not_exists(folder_path): os.makedirs(direc) -def cmd_exists(c): +def cmd_exists(c: str) -> bool: return subprocess.call("type " + c, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 @@ -234,7 +243,7 @@ def cmd_exists(c): ################ -def initialize_pymol(options): +def initialize_pymol(options: str) -> None: """Initializes PyMOL""" import pymol # Pass standard arguments of function to prevent PyMOL from printing out PDB headers (workaround) @@ -242,7 +251,7 @@ def initialize_pymol(options): pymol.cmd.reinitialize() -def start_pymol(quiet=False, options='-p', run=False): +def start_pymol(quiet: bool = False, options: str = '-p', run: bool = False) -> None: """Starts up PyMOL and sets general options. Quiet mode suppresses all PyMOL output. Command line options can be passed as the second argument.""" import pymol @@ -253,7 +262,7 @@ def start_pymol(quiet=False, options='-p', run=False): pymol.cmd.feedback('disable', 'all', 'everything') -def select_region(region): +def select_region(region: Region) -> str: """region is a dictionary with chains as keys and a list of residue numbers as values.""" selection = f"(chain " for i, (chain, res_numbers) in enumerate(region.items()): @@ -265,7 +274,7 @@ def select_region(region): return selection -def nucleotide_linkage(residues): +def nucleotide_linkage(residues: dict[LigandMember, object]) -> list[CovalentLink]: """Support for DNA/RNA ligands by finding missing covalent linkages to stitch DNA/RNA together.""" nuc_covalent = [] @@ -274,7 +283,6 @@ def nucleotide_linkage(residues): ####################################### nucleotides = ['A', 'C', 'T', 'G', 'U', 'DA', 'DC', 'DT', 'DG', 'DU'] dna_rna = {} # Dictionary of DNA/RNA residues by chain - covlinkage = namedtuple("covlinkage", "id1 chain1 pos1 conf1 id2 chain2 pos2 conf2") # Create missing covlinkage entries for DNA/RNA for ligand in residues: resname, chain, pos = ligand @@ -290,14 +298,14 @@ def nucleotide_linkage(residues): name, pos = nucleotide nextnucleotide = nuc_list[i + 1] nextname, nextpos = nextnucleotide - newlink = covlinkage(id1=name, chain1=chain, pos1=pos, conf1='', - id2=nextname, chain2=chain, pos2=nextpos, conf2='') + newlink = CovalentLink(id1=name, chain1=chain, pos1=pos, conf1='', + id2=nextname, chain2=chain, pos2=nextpos, conf2='') nuc_covalent.append(newlink) return nuc_covalent -def ring_is_planar(ring, r_atoms): +def ring_is_planar(ring: OBRing, r_atoms: Sequence[Atom]) -> bool: """Given a set of ring atoms, check if the ring is sufficiently planar to be considered aromatic""" normals = [] @@ -315,7 +323,7 @@ def ring_is_planar(ring, r_atoms): return True -def classify_by_name(names): +def classify_by_name(names: Sequence[str]) -> str: """Classify a (composite) ligand by the HETID(s)""" if len(names) > 3: # Polymer if len(set(config.RNA).intersection(set(names))) != 0: @@ -337,7 +345,7 @@ def classify_by_name(names): return ligtype -def sort_members_by_importance(members): +def sort_members_by_importance(members: Sequence[LigandMember]) -> list[LigandMember]: """Sort the members of a composite ligand according to two criteria: 1. Split up in main and ion group. Ion groups are located behind the main group. 2. Within each group, sort by chain and position.""" @@ -348,7 +356,10 @@ def sort_members_by_importance(members): return sorted_main + sorted_ion -def get_isomorphisms(reference, lig): +def get_isomorphisms( + reference: pybel.Molecule, + lig: pybel.Molecule, +) -> Iterable[Iterable[tuple[int, int]]]: """Get all isomorphisms of the ligand.""" query = pybel.ob.CompileMoleculeQuery(reference.OBMol) mappr = pybel.ob.OBIsomorphismMapper.GetInstance(query) @@ -364,7 +375,7 @@ def get_isomorphisms(reference, lig): return isomorphs -def canonicalize(lig, preserve_bond_order=False): +def canonicalize(lig: pybel.Molecule, preserve_bond_order: bool = False) -> list[int] | None: """Get the canonical atom order for the ligand.""" atomorder = None # Get canonical atom order @@ -396,7 +407,7 @@ def canonicalize(lig, preserve_bond_order=False): return atomorder -def int32_to_negative(int32): +def int32_to_negative(int32: int) -> int: """Checks if a suspicious number (e.g. ligand position) is in fact a negative number represented as a 32 bit integer and returns the actual number. """ @@ -411,13 +422,13 @@ def int32_to_negative(int32): return int32 -def read_pdb(pdbfname, as_string=False): +def read_pdb(pdbfname: str, as_string: bool = False) -> tuple[pybel.Molecule, str]: """Reads a given PDB file and returns a Pybel Molecule.""" pybel.ob.obErrorLog.StopLogging() # Suppress all OpenBabel warnings return readmol(pdbfname, as_string=as_string) -def read(fil): +def read(fil: str) -> IO[str] | gzip.GzipFile | zipfile.ZipExtFile: """Returns a file handler and detects gzipped files.""" if os.path.splitext(fil)[-1] == '.gz': return gzip.open(fil, 'rb') @@ -428,7 +439,7 @@ def read(fil): return open(fil, 'r') -def readmol(path, as_string=False): +def readmol(path: str, as_string: bool = False) -> tuple[pybel.Molecule, str]: """Reads the given molecule file and returns the corresponding Pybel molecule as well as the input file type. In contrast to the standard Pybel implementation, the file is closed properly.""" supported_formats = ['pdb'] diff --git a/plip/exchange/report.py b/plip/exchange/report.py index bfb89e4..4569b93 100644 --- a/plip/exchange/report.py +++ b/plip/exchange/report.py @@ -1,18 +1,20 @@ import time +from collections.abc import Sequence from operator import itemgetter import lxml.etree as et import gzip +from typing import TextIO from plip.basic import config from plip.basic.config import __version__ -from plip.structure.preparation import PDBComplex +from plip.structure.preparation import PDBComplex, PLInteraction class StructureReport: """Creates reports (xml or txt) for one structure/""" - def __init__(self, mol: PDBComplex, outputprefix: str = 'report'): + def __init__(self, mol: PDBComplex, outputprefix: str = 'report') -> None: self.mol = mol self.excluded = self.mol.excluded self.xmlreport = self.construct_xml_tree() @@ -21,7 +23,7 @@ def __init__(self, mol: PDBComplex, outputprefix: str = 'report'): self.outpath = mol.output_path self.outputprefix = outputprefix - def construct_xml_tree(self): + def construct_xml_tree(self) -> et._Element: """Construct the basic XML tree""" report = et.Element('report') plipversion = et.SubElement(report, 'plipversion') @@ -63,7 +65,7 @@ def construct_xml_tree(self): f2.text = ":".join([covlinkage.id2, covlinkage.chain2, str(covlinkage.pos2)]) return report - def construct_txt_file(self): + def construct_txt_file(self) -> list[str]: """Construct the header of the txt file""" textlines = ['Prediction of noncovalent interactions for PDB structure %s' % self.mol.pymol_name.upper(), ] textlines.append("=" * len(textlines[0])) @@ -77,7 +79,7 @@ def construct_txt_file(self): textlines.append(f'Analysis was done on model {config.MODEL}.\n') return textlines - def get_bindingsite_data(self): + def get_bindingsite_data(self) -> None: """Get the additional data for the binding sites""" for i, site in enumerate(sorted(self.mol.interaction_sets)): s = self.mol.interaction_sets[site] @@ -92,7 +94,7 @@ def get_bindingsite_data(self): else: self.txtreport.append('No interactions detected.') - def write_xml(self, as_string: bool = False): + def write_xml(self, as_string: bool = False) -> None: """Write the XML report""" if not as_string: tree = et.ElementTree(self.xmlreport) @@ -106,7 +108,7 @@ def write_xml(self, as_string: bool = False): output = et.tostring(self.xmlreport, pretty_print=True) print(output.decode('utf8')) - def write_txt(self, as_string: bool = False): + def write_txt(self, as_string: bool = False) -> None: """Write the TXT report""" if not as_string: if config.COMPRESS: @@ -125,7 +127,7 @@ def write_txt(self, as_string: bool = False): class BindingSiteReport: """Gather report data and generate reports for one binding site in different formats.""" - def __init__(self, plcomplex): + def __init__(self, plcomplex: PLInteraction) -> None: ################ # GENERAL DATA # @@ -313,7 +315,7 @@ def __init__(self, plcomplex): m.target.atom.coords)) @staticmethod - def write_section(name, features, info, f): + def write_section(name: str, features: list[str], info: list, f: TextIO) -> None: """Provides formatting for one section (e.g. hydrogen bonds)""" if not len(info) == 0: f.write('\n\n### %s ###\n' % name) @@ -322,7 +324,7 @@ def write_section(name, features, info, f): f.write('%s\n' % '\t'.join(map(str, line))) @staticmethod - def rst_table(array): + def rst_table(array: Sequence[Sequence[str]]) -> str: """Given an array, the function formats and returns and table in rST format.""" # Determine cell width for each column cell_dict = {} @@ -363,7 +365,7 @@ def rst_table(array): form += '\n' return form - def generate_txt(self): + def generate_txt(self) -> list[str]: """Generates an flat text report for a single binding site""" txt = [] @@ -402,7 +404,7 @@ def generate_txt(self): txt.append('\n') return txt - def generate_xml(self): + def generate_xml(self) -> et._Element: """Generates an XML-formatted report for a single binding site""" report = et.Element('bindingsite') identifiers = et.SubElement(report, 'identifiers') @@ -464,7 +466,11 @@ def generate_xml(self): m.text = bsid interactions = et.SubElement(report, 'interactions') - def format_interactions(element_name, features, interaction_information): + def format_interactions( + element_name: str, + features: Sequence[str], + interaction_information: list, + ) -> et._Element: """Returns a formatted element with interaction information.""" interaction = et.Element(element_name) # Sort results first by res number, then by distance and finally ligand coordinates to get a unique order diff --git a/plip/exchange/webservices.py b/plip/exchange/webservices.py index 08fff37..93097c3 100644 --- a/plip/exchange/webservices.py +++ b/plip/exchange/webservices.py @@ -9,7 +9,7 @@ logger = logger.get_logger() -def check_pdb_status(pdbid): +def check_pdb_status(pdbid: str) -> list[str | None]: """Returns the status and up-to-date entry in the PDB for a given PDB ID""" url = 'http://www.rcsb.org/pdb/rest/idStatus?structureId=%s' % pdbid xmlf = urlopen(url) @@ -24,7 +24,7 @@ def check_pdb_status(pdbid): return [status, current_pdbid.lower()] -def fetch_pdb(pdbid): +def fetch_pdb(pdbid: str) -> list[str]: """Get the newest entry from the RCSB server for the given PDB ID. Exits with '1' if PDB ID is invalid.""" pdbid = pdbid.lower() # logger.info(f'checking status of PDB-ID {pdbid}') diff --git a/plip/exchange/xml.py b/plip/exchange/xml.py index f014f42..0163353 100644 --- a/plip/exchange/xml.py +++ b/plip/exchange/xml.py @@ -5,7 +5,11 @@ class XMLStorage: """Generic class for storing XML data from PLIP XML files.""" @staticmethod - def getdata(tree, location, force_string=False): + def getdata( + tree: etree._Element | etree._ElementTree, + location: str, + force_string: bool = False, + ) -> str | int | float | bool | None: """Gets XML data from a specific element and handles types.""" found = tree.xpath('%s/text()' % location) if not found: @@ -29,7 +33,7 @@ def getdata(tree, location, force_string=False): return data @staticmethod - def getcoordinates(tree, location): + def getcoordinates(tree: etree._Element, location: str) -> tuple[float, ...]: """Gets coordinates from a specific element in PLIP XML""" return tuple(float(x) for x in tree.xpath('.//%s/*/text()' % location)) @@ -37,7 +41,7 @@ def getcoordinates(tree, location): class Interaction(XMLStorage): """Stores information on a specific interaction type""" - def __init__(self, interaction_part): + def __init__(self, interaction_part: etree._Element) -> None: self.id = interaction_part.get('id') self.resnr = self.getdata(interaction_part, 'resnr') self.restype = self.getdata(interaction_part, 'restype', force_string=True) @@ -52,7 +56,7 @@ def __init__(self, interaction_part): class HydrophobicInteraction(Interaction): """Stores information on a hydrophobic interaction""" - def __init__(self, hydrophobic_part): + def __init__(self, hydrophobic_part: etree._Element) -> None: Interaction.__init__(self, hydrophobic_part) self.dist = self.getdata(hydrophobic_part, 'dist') self.ligcarbonidx = self.getdata(hydrophobic_part, 'ligcarbonidx') @@ -62,7 +66,7 @@ def __init__(self, hydrophobic_part): class HydrogenBond(Interaction): """Stores information on a hydrogen bond interaction""" - def __init__(self, hbond_part): + def __init__(self, hbond_part: etree._Element) -> None: Interaction.__init__(self, hbond_part) self.sidechain = self.getdata(hbond_part, 'sidechain') self.dist_h_a = self.getdata(hbond_part, 'dist_h-a') @@ -80,7 +84,7 @@ def __init__(self, hbond_part): class WaterBridge(Interaction): """Stores information on a water bridge interaction""" - def __init__(self, wbridge_part): + def __init__(self, wbridge_part: etree._Element) -> None: Interaction.__init__(self, wbridge_part) self.dist_a_w = self.getdata(wbridge_part, 'dist_a-w') self.dist_d_w = self.getdata(wbridge_part, 'dist_d-w') @@ -100,7 +104,7 @@ def __init__(self, wbridge_part): class SaltBridge(Interaction): """Stores information on a salt bridge interaction""" - def __init__(self, sbridge_part): + def __init__(self, sbridge_part: etree._Element) -> None: Interaction.__init__(self, sbridge_part) self.dist = self.getdata(sbridge_part, 'dist') self.protispos = self.getdata(sbridge_part, 'protispos') @@ -114,7 +118,7 @@ def __init__(self, sbridge_part): class PiStacking(Interaction): """Stores information on a pi stacking interaction""" - def __init__(self, pistack_part): + def __init__(self, pistack_part: etree._Element) -> None: Interaction.__init__(self, pistack_part) self.centdist = self.getdata(pistack_part, 'centdist') self.dist = self.centdist @@ -130,7 +134,7 @@ def __init__(self, pistack_part): class PiCation(Interaction): """Stores information on a pi cation interaction""" - def __init__(self, pication_part): + def __init__(self, pication_part: etree._Element) -> None: Interaction.__init__(self, pication_part) self.dist = self.getdata(pication_part, 'dist') self.offset = self.getdata(pication_part, 'offset') @@ -142,7 +146,7 @@ def __init__(self, pication_part): class HalogenBond(Interaction): """Stores information on a halogen bond interaction""" - def __init__(self, halogen_part): + def __init__(self, halogen_part: etree._Element) -> None: Interaction.__init__(self, halogen_part) self.dist = self.getdata(halogen_part, 'dist') self.don_angle = self.getdata(halogen_part, 'don_angle') @@ -157,7 +161,7 @@ def __init__(self, halogen_part): class MetalComplex(Interaction): """Stores information on a metal complexe interaction""" - def __init__(self, metalcomplex_part): + def __init__(self, metalcomplex_part: etree._Element) -> None: Interaction.__init__(self, metalcomplex_part) self.metal_idx = self.getdata(metalcomplex_part, 'metal_idx') self.metal_type = self.getdata(metalcomplex_part, 'metal_type', force_string=True) @@ -176,7 +180,7 @@ def __init__(self, metalcomplex_part): class BSite(XMLStorage): """Stores all information about an specific binding site.""" - def __init__(self, bindingsite, pdbid): + def __init__(self, bindingsite: etree._Element, pdbid: str) -> None: self.bindingsite = bindingsite self.pdbid = pdbid self.bsid = ":".join(bindingsite.xpath('identifiers/*/text()')[2:5]) @@ -241,7 +245,7 @@ def __init__(self, bindingsite, pdbid): self.get_atom_mapping() self.counts = self.get_counts() - def get_atom_mapping(self): + def get_atom_mapping(self) -> None: """Parses the ligand atom mapping.""" # Atom mappings smiles_to_pdb_mapping = self.bindingsite.xpath('mappings/smiles_to_pdb/text()') @@ -253,7 +257,7 @@ def get_atom_mapping(self): self.mappings = {'smiles_to_pdb': smiles_to_pdb_mapping} self.mappings['pdb_to_smiles'] = {v: k for k, v in self.mappings['smiles_to_pdb'].items()} - def get_counts(self): + def get_counts(self) -> dict[str, int]: """counts the interaction types and backbone hydrogen bonding in a binding site""" hbondsback = len([hb for hb in self.hbonds if not hb.sidechain]) @@ -270,7 +274,7 @@ def get_counts(self): class PlipXML(XMLStorage): """Parses and stores all information from a PLIP XML file.""" - def __init__(self, xmlfile): + def __init__(self, xmlfile: str) -> None: self.load_data(xmlfile) # Parse general information @@ -285,6 +289,6 @@ def __init__(self, xmlfile): self.bsites = {BSite(bs, self.pdbid).bsid: BSite(bs, self.pdbid) for bs in self.doc.xpath('//bindingsite')} self.num_bsites = len(self.bsites) - def load_data(self, xmlfile): + def load_data(self, xmlfile: str) -> None: """Loads/parses an XML file and saves it as a tree if successful.""" self.doc = etree.parse(xmlfile) diff --git a/plip/plipcmd.py b/plip/plipcmd.py index 6f44215..c09c48e 100644 --- a/plip/plipcmd.py +++ b/plip/plipcmd.py @@ -12,7 +12,7 @@ import sys import ast from argparse import ArgumentParser -from collections import namedtuple +from typing import NamedTuple from plip.basic import config, logger @@ -33,14 +33,19 @@ f"Supported and maintained by: {config.__maintainer__}" -def threshold_limiter(aparser, arg): - arg = float(arg) - if arg <= 0: +class Threshold(NamedTuple): + name: str + type: str + + +def threshold_limiter(aparser: ArgumentParser, arg: str) -> float: + value = float(arg) + if value <= 0: aparser.error("All thresholds have to be values larger than zero.") - return arg + return value -def parse_report_filename(parser, name_config): +def parse_report_filename(parser: ArgumentParser, name_config: str | None) -> str | None: if name_config is not None: dir_part, name_config = os.path.split(name_config) if not name_config: # provided filename is a directory. @@ -60,7 +65,7 @@ def parse_report_filename(parser, name_config): return name_config -def process_pdb(pdbfile, outpath, as_string=False, batch_idx=None): +def process_pdb(pdbfile: str, outpath: str, as_string: bool = False, batch_idx: int | None = None) -> None: """Analysis of a single PDB file with optional chain filtering.""" if not as_string: pdb_file_name = pdbfile.split('/')[-1] @@ -109,7 +114,7 @@ def process_pdb(pdbfile, outpath, as_string=False, batch_idx=None): streport.write_txt(as_string=config.STDOUT) -def download_structure(inputpdbid): +def download_structure(inputpdbid: str) -> tuple[str, str]: """Given a PDB ID, downloads the corresponding PDB structure. Checks for validity of ID and handles error while downloading. Returns the path of the downloaded file.""" @@ -130,7 +135,7 @@ def download_structure(inputpdbid): sys.exit(1) -def remove_duplicates(slist): +def remove_duplicates(slist: list[str]) -> list[str]: """Checks input lists for duplicates and returns a list with unique entries""" unique = list(set(slist)) @@ -142,7 +147,7 @@ def remove_duplicates(slist): return unique -def run_analysis(inputstructs, inputpdbids): +def run_analysis(inputstructs: list[str] | None, inputpdbids: list[str] | None) -> None: """Main function. Calls functions for processing, report generation and visualization.""" pdbid, pdbpath = None, None batch_idx = None @@ -188,7 +193,7 @@ def run_analysis(inputstructs, inputpdbids): logger.info(f'finished analysis, find the result files in {config.BASEPATH}') -def main(): +def main() -> None: """Parse command line arguments and start main script for analysis.""" parser = ArgumentParser(prog="PLIP", description=description) pdbstructure = parser.add_mutually_exclusive_group(required=True) # Needs either PDB ID or file @@ -252,17 +257,16 @@ def main(): parser.add_argument("--model", dest="model", default=1, type=int, help="Model number to be used for multi-model structures.") # Optional threshold arguments, not shown in help - thr = namedtuple('threshold', 'name type') - thresholds = [thr(name='aromatic_planarity', type='angle'), - thr(name='hydroph_dist_max', type='distance'), thr(name='hbond_dist_max', type='distance'), - thr(name='hbond_don_angle_min', type='angle'), thr(name='pistack_dist_max', type='distance'), - thr(name='pistack_ang_dev', type='other'), thr(name='pistack_offset_max', type='distance'), - thr(name='pication_dist_max', type='distance'), thr(name='saltbridge_dist_max', type='distance'), - thr(name='halogen_dist_max', type='distance'), thr(name='halogen_acc_angle', type='angle'), - thr(name='halogen_don_angle', type='angle'), thr(name='halogen_angle_dev', type='other'), - thr(name='water_bridge_mindist', type='distance'), thr(name='water_bridge_maxdist', type='distance'), - thr(name='water_bridge_omega_min', type='angle'), thr(name='water_bridge_omega_max', type='angle'), - thr(name='water_bridge_theta_min', type='angle')] + thresholds = [Threshold(name='aromatic_planarity', type='angle'), + Threshold(name='hydroph_dist_max', type='distance'), Threshold(name='hbond_dist_max', type='distance'), + Threshold(name='hbond_don_angle_min', type='angle'), Threshold(name='pistack_dist_max', type='distance'), + Threshold(name='pistack_ang_dev', type='other'), Threshold(name='pistack_offset_max', type='distance'), + Threshold(name='pication_dist_max', type='distance'), Threshold(name='saltbridge_dist_max', type='distance'), + Threshold(name='halogen_dist_max', type='distance'), Threshold(name='halogen_acc_angle', type='angle'), + Threshold(name='halogen_don_angle', type='angle'), Threshold(name='halogen_angle_dev', type='other'), + Threshold(name='water_bridge_mindist', type='distance'), Threshold(name='water_bridge_maxdist', type='distance'), + Threshold(name='water_bridge_omega_min', type='angle'), Threshold(name='water_bridge_omega_max', type='angle'), + Threshold(name='water_bridge_theta_min', type='angle')] for t in thresholds: parser.add_argument('--%s' % t.name, dest=t.name, type=lambda val: threshold_limiter(parser, val), help=argparse.SUPPRESS) @@ -321,7 +325,7 @@ def main(): config.NOHYDRO = arguments.nohydro config.MODEL = arguments.model - def expand_ranges(residue_ranges): + def expand_ranges(residue_ranges: str) -> list[int]: """ Takes '1-3, 5, 7' -> [1, 2, 3, 5, 7] """ diff --git a/plip/structure/detection.py b/plip/structure/detection.py index 7af526b..a472ff1 100644 --- a/plip/structure/detection.py +++ b/plip/structure/detection.py @@ -1,6 +1,6 @@ import itertools from collections import defaultdict -from collections import namedtuple +from typing import TypeVar import numpy as np from openbabel.openbabel import OBAtomAtomIter @@ -8,10 +8,35 @@ from plip.basic import config, logger from plip.basic.supplemental import vecangle, vector, euclidean3d, projection from plip.basic.supplemental import whichresnumber, whichrestype, whichchain +from plip.structure.records import ( + AromaticRing, + GeometryFit, + HalogenBond, + HalogenBondAcceptor, + HalogenBondDonor, + HydrogenBond, + HydrogenBondAcceptor, + HydrogenBondDonor, + HydrophobicAtom, + HydrophobicInteraction, + LigandCharge, + MetalAtom, + MetalBinding, + MetalComplex, + PiCationInteraction, + PiStack, + ProteinCharge, + SaltBridge, + WaterBridge, + WaterMolecule, +) logger = logger.get_logger() -def filter_contacts(pairings): +Contact = TypeVar("Contact") + + +def filter_contacts(pairings: list[Contact]) -> list[Contact]: """Filter interactions by two criteria: 1. No interactions between the same residue (important for intra mode). 2. No duplicate interactions (A with B and B with A, also important for intra mode).""" @@ -41,12 +66,13 @@ def filter_contacts(pairings): # FUNCTIONS FOR DETECTION OF SPECIFIC INTERACTIONS ################################################## -def hydrophobic_interactions(atom_set_a, atom_set_b): +def hydrophobic_interactions( + atom_set_a: list[HydrophobicAtom], + atom_set_b: list[HydrophobicAtom], +) -> list[HydrophobicInteraction]: """Detection of hydrophobic pliprofiler between atom_set_a (binding site) and atom_set_b (ligand). Definition: All pairs of qualified carbon atoms within a distance of HYDROPH_DIST_MAX """ - data = namedtuple('hydroph_interaction', 'bsatom bsatom_orig_idx ligatom ligatom_orig_idx ' - 'distance restype resnr reschain restype_l, resnr_l, reschain_l') pairings = [] for a, b in itertools.product(atom_set_a, atom_set_b): if a.orig_idx == b.orig_idx: @@ -56,22 +82,25 @@ def hydrophobic_interactions(atom_set_a, atom_set_b): continue restype, resnr, reschain = whichrestype(a.atom), whichresnumber(a.atom), whichchain(a.atom) restype_l, resnr_l, reschain_l = whichrestype(b.orig_atom), whichresnumber(b.orig_atom), whichchain(b.orig_atom) - contact = data(bsatom=a.atom, bsatom_orig_idx=a.orig_idx, ligatom=b.atom, ligatom_orig_idx=b.orig_idx, - distance=e, restype=restype, resnr=resnr, - reschain=reschain, restype_l=restype_l, - resnr_l=resnr_l, reschain_l=reschain_l) + contact = HydrophobicInteraction( + bsatom=a.atom, bsatom_orig_idx=a.orig_idx, ligatom=b.atom, ligatom_orig_idx=b.orig_idx, + distance=e, restype=restype, resnr=resnr, reschain=reschain, + restype_l=restype_l, resnr_l=resnr_l, reschain_l=reschain_l) pairings.append(contact) return filter_contacts(pairings) -def hbonds(acceptors, donor_pairs, protisdon, typ): +def hbonds( + acceptors: list[HydrogenBondAcceptor], + donor_pairs: list[HydrogenBondDonor], + protisdon: bool, + typ: str, +) -> list[HydrogenBond]: """Detection of hydrogen bonds between sets of acceptors and donor pairs. Definition: All pairs of hydrogen bond acceptor and donors with donor hydrogens and acceptor showing a distance within HBOND DIST MIN and HBOND DIST MAX and donor angles above HBOND_DON_ANGLE_MIN """ - data = namedtuple('hbond', 'a a_orig_idx d d_orig_idx h distance_ah distance_ad angle type protisdon resnr ' - 'restype reschain resnr_l restype_l reschain_l sidechain atype dtype') pairings = [] for acc, don in itertools.product(acceptors, donor_pairs): if not typ == 'strong': @@ -102,20 +131,18 @@ def hbonds(acceptors, donor_pairs, protisdon, typ): 8) and ligatom.GetResidue().GetAtomProperty( ligatom, 8): continue - contact = data(a=acc.a, a_orig_idx=acc.a_orig_idx, d=don.d, d_orig_idx=don.d_orig_idx, h=don.h, - distance_ah=dist_ah, distance_ad=dist_ad, angle=v, type=typ, protisdon=protisdon, - resnr=resnr, restype=restype, reschain=reschain, resnr_l=resnr_l, - restype_l=restype_l, reschain_l=rechain_l, sidechain=is_sidechain_hbond, - atype=acc.a.type, dtype=don.d.type) + contact = HydrogenBond( + a=acc.a, a_orig_idx=acc.a_orig_idx, d=don.d, d_orig_idx=don.d_orig_idx, h=don.h, + distance_ah=dist_ah, distance_ad=dist_ad, angle=v, type=typ, protisdon=protisdon, + resnr=resnr, restype=restype, reschain=reschain, resnr_l=resnr_l, + restype_l=restype_l, reschain_l=rechain_l, sidechain=is_sidechain_hbond, + atype=acc.a.type, dtype=don.d.type) pairings.append(contact) return filter_contacts(pairings) -def pistacking(rings_bs, rings_lig): +def pistacking(rings_bs: list[AromaticRing], rings_lig: list[AromaticRing]) -> list[PiStack]: """Return all pi-stackings between the given aromatic ring systems in receptor and ligand.""" - data = namedtuple( - 'pistack', - 'proteinring ligandring distance angle offset type restype resnr reschain restype_l resnr_l reschain_l') pairings = [] for r, l in itertools.product(rings_bs, rings_lig): # DISTANCE AND RING ANGLE CALCULATION @@ -144,19 +171,22 @@ def pistacking(rings_bs, rings_lig): ptype = 'T' passed = True if passed: - contact = data(proteinring=r, ligandring=l, distance=d, angle=a, offset=offset, - type=ptype, resnr=resnr, restype=restype, reschain=reschain, - resnr_l=resnr_l, restype_l=restype_l, reschain_l=reschain_l) + contact = PiStack( + proteinring=r, ligandring=l, distance=d, angle=a, offset=offset, + type=ptype, resnr=resnr, restype=restype, reschain=reschain, + resnr_l=resnr_l, restype_l=restype_l, reschain_l=reschain_l) pairings.append(contact) return filter_contacts(pairings) -def pication(rings, pos_charged, protcharged): +def pication( + rings: list[AromaticRing], + pos_charged: list[ProteinCharge | LigandCharge], + protcharged: bool, +) -> list[PiCationInteraction]: """Return all pi-Cation interaction between aromatic rings and positively charged groups. For tertiary and quaternary amines, check also the angle between the ring and the nitrogen. """ - data = namedtuple( - 'pication', 'ring charge distance offset type restype resnr reschain restype_l resnr_l reschain_l protcharged') pairings = [] if len(rings) == 0 or len(pos_charged) == 0: return pairings @@ -169,7 +199,7 @@ def pication(rings, pos_charged, protcharged): offset = euclidean3d(proj, ring.center) if not config.MIN_DIST < d < config.PICATION_DIST_MAX or not offset < config.PISTACK_OFFSET_MAX: continue - if type(p).__name__ == 'lcharge' and p.fgroup == 'tertamine': + if isinstance(p, LigandCharge) and p.fgroup == 'tertamine': # Special case here if the ligand has a tertiary amine, check an additional angle # Otherwise, we might have have a pi-cation interaction 'through' the ligand n_atoms = [a_neighbor for a_neighbor in OBAtomAtomIter(p.atoms[0].OBAtom)] @@ -184,10 +214,11 @@ def pication(rings, pos_charged, protcharged): reschain = whichchain(ring.atoms[0]) resnr_l, restype_l = whichresnumber(p.orig_atoms[0]), whichrestype(p.orig_atoms[0]) reschain_l = whichchain(p.orig_atoms[0]) - contact = data(ring=ring, charge=p, distance=d, offset=offset, type='regular', - restype=restype, resnr=resnr, reschain=reschain, - restype_l=restype_l, resnr_l=resnr_l, reschain_l=reschain_l, - protcharged=protcharged) + contact = PiCationInteraction( + ring=ring, charge=p, distance=d, offset=offset, type='regular', + restype=restype, resnr=resnr, reschain=reschain, + restype_l=restype_l, resnr_l=resnr_l, reschain_l=reschain_l, + protcharged=protcharged) pairings.append(contact) break resnr = whichresnumber(p.atoms[0]) if protcharged else whichresnumber(ring.atoms[0]) @@ -196,17 +227,20 @@ def pication(rings, pos_charged, protcharged): restype_l = whichrestype(ring.orig_atoms[0]) if protcharged else whichrestype(p.orig_atoms[0]) reschain = whichchain(p.atoms[0]) if protcharged else whichchain(ring.atoms[0]) reschain_l = whichchain(ring.orig_atoms[0]) if protcharged else whichchain(p.orig_atoms[0]) - contact = data(ring=ring, charge=p, distance=d, offset=offset, type='regular', restype=restype, - resnr=resnr, reschain=reschain, restype_l=restype_l, resnr_l=resnr_l, - reschain_l=reschain_l, protcharged=protcharged) + contact = PiCationInteraction( + ring=ring, charge=p, distance=d, offset=offset, type='regular', restype=restype, + resnr=resnr, reschain=reschain, restype_l=restype_l, resnr_l=resnr_l, + reschain_l=reschain_l, protcharged=protcharged) pairings.append(contact) return filter_contacts(pairings) -def saltbridge(poscenter, negcenter, protispos): +def saltbridge( + poscenter: list[ProteinCharge | LigandCharge], + negcenter: list[ProteinCharge | LigandCharge], + protispos: bool, +) -> list[SaltBridge]: """Detect all salt bridges (pliprofiler between centers of positive and negative charge)""" - data = namedtuple( - 'saltbridge', 'positive negative distance protispos resnr restype reschain resnr_l restype_l reschain_l') pairings = [] for pc, nc in itertools.product(poscenter, negcenter): if not config.MIN_DIST < euclidean3d(pc.center, nc.center) < config.SALTBRIDGE_DIST_MAX: @@ -217,17 +251,19 @@ def saltbridge(poscenter, negcenter, protispos): restype_l = whichrestype(nc.orig_atoms[0]) if protispos else whichrestype(pc.orig_atoms[0]) reschain = pc.reschain if protispos else nc.reschain reschain_l = whichchain(nc.orig_atoms[0]) if protispos else whichchain(pc.orig_atoms[0]) - contact = data(positive=pc, negative=nc, distance=euclidean3d(pc.center, nc.center), protispos=protispos, - resnr=resnr, restype=restype, reschain=reschain, resnr_l=resnr_l, restype_l=restype_l, - reschain_l=reschain_l) + contact = SaltBridge( + positive=pc, negative=nc, distance=euclidean3d(pc.center, nc.center), protispos=protispos, + resnr=resnr, restype=restype, reschain=reschain, resnr_l=resnr_l, + restype_l=restype_l, reschain_l=reschain_l) pairings.append(contact) return filter_contacts(pairings) -def halogen(acceptor, donor): +def halogen( + acceptor: list[HalogenBondAcceptor], + donor: list[HalogenBondDonor], +) -> list[HalogenBond]: """Detect all halogen bonds of the type Y-O...X-C""" - data = namedtuple('halogenbond', 'acc acc_orig_idx don don_orig_idx distance don_angle acc_angle restype ' - 'resnr reschain restype_l resnr_l reschain_l donortype acctype sidechain') pairings = [] for acc, don in itertools.product(acceptor, donor): dist = euclidean3d(acc.o.coords, don.x.coords) @@ -245,20 +281,24 @@ def halogen(acceptor, donor): continue restype, reschain, resnr = whichrestype(acc.o), whichchain(acc.o), whichresnumber(acc.o) restype_l, reschain_l, resnr_l = whichrestype(don.orig_x), whichchain(don.orig_x), whichresnumber(don.orig_x) - contact = data(acc=acc, acc_orig_idx=acc.o_orig_idx, don=don, don_orig_idx=don.x_orig_idx, - distance=dist, don_angle=don_angle, acc_angle=acc_angle, - restype=restype, resnr=resnr, - reschain=reschain, restype_l=restype_l, - reschain_l=reschain_l, resnr_l=resnr_l, donortype=don.x.OBAtom.GetType(), acctype=acc.o.type, - sidechain=is_sidechain_hal) + contact = HalogenBond( + acc=acc, acc_orig_idx=acc.o_orig_idx, don=don, don_orig_idx=don.x_orig_idx, + distance=dist, don_angle=don_angle, acc_angle=acc_angle, + restype=restype, resnr=resnr, reschain=reschain, restype_l=restype_l, + reschain_l=reschain_l, resnr_l=resnr_l, donortype=don.x.OBAtom.GetType(), + acctype=acc.o.type, sidechain=is_sidechain_hal) pairings.append(contact) return filter_contacts(pairings) -def water_bridges(bs_hba, lig_hba, bs_hbd, lig_hbd, water): +def water_bridges( + bs_hba: list[HydrogenBondAcceptor], + lig_hba: list[HydrogenBondAcceptor], + bs_hbd: list[HydrogenBondDonor], + lig_hbd: list[HydrogenBondDonor], + water: list[WaterMolecule], +) -> list[WaterBridge]: """Find water-bridged hydrogen bonds between ligand and protein. For now only considers bridged of first degree.""" - data = namedtuple('waterbridge', 'a a_orig_idx atype d d_orig_idx dtype h water water_orig_idx distance_aw ' - 'distance_dw d_angle w_angle type resnr restype reschain resnr_l restype_l reschain_l protisdon') pairings = [] # First find all acceptor-water pairs with distance within d # and all donor-water pairs with distance within d and angle greater theta @@ -297,11 +337,12 @@ def water_bridges(bs_hba, lig_hba, bs_hbd, lig_hbd, water): resnr, reschain, restype = whichresnumber(don.d), whichchain(don.d), whichrestype(don.d) resnr_l, reschain_l, restype_l = whichresnumber(acc.a_orig_atom), whichchain( acc.a_orig_atom), whichrestype(acc.a_orig_atom) - contact = data(a=acc.a, a_orig_idx=acc.a_orig_idx, atype=acc.a.type, d=don.d, d_orig_idx=don.d_orig_idx, - dtype=don.d.type, h=don.h, water=wl.oxy, water_orig_idx=wl.oxy_orig_idx, - distance_aw=distance_aw, distance_dw=distance_dw, d_angle=d_angle, w_angle=w_angle, - type='first_deg', resnr=resnr, restype=restype, - reschain=reschain, restype_l=restype_l, resnr_l=resnr_l, reschain_l=reschain_l, protisdon=True) + contact = WaterBridge( + a=acc.a, a_orig_idx=acc.a_orig_idx, atype=acc.a.type, d=don.d, d_orig_idx=don.d_orig_idx, + dtype=don.d.type, h=don.h, water=wl.oxy, water_orig_idx=wl.oxy_orig_idx, + distance_aw=distance_aw, distance_dw=distance_dw, d_angle=d_angle, w_angle=w_angle, + type='first_deg', resnr=resnr, restype=restype, reschain=reschain, + restype_l=restype_l, resnr_l=resnr_l, reschain_l=reschain_l, protisdon=True) pairings.append(contact) for p, l in itertools.product(prot_aw, lig_dw): acc, wl, distance_aw = p @@ -315,21 +356,23 @@ def water_bridges(bs_hba, lig_hba, bs_hbd, lig_hbd, water): resnr, reschain, restype = whichresnumber(acc.a), whichchain(acc.a), whichrestype(acc.a) resnr_l, reschain_l, restype_l = whichresnumber(don.d_orig_atom), whichchain( don.d_orig_atom), whichrestype(don.d_orig_atom) - contact = data(a=acc.a, a_orig_idx=acc.a_orig_idx, atype=acc.a.type, d=don.d, d_orig_idx=don.d_orig_idx, - dtype=don.d.type, h=don.h, water=wl.oxy, water_orig_idx=wl.oxy_orig_idx, - distance_aw=distance_aw, distance_dw=distance_dw, - d_angle=d_angle, w_angle=w_angle, type='first_deg', resnr=resnr, - restype=restype, reschain=reschain, - restype_l=restype_l, reschain_l=reschain_l, resnr_l=resnr_l, protisdon=False) + contact = WaterBridge( + a=acc.a, a_orig_idx=acc.a_orig_idx, atype=acc.a.type, d=don.d, d_orig_idx=don.d_orig_idx, + dtype=don.d.type, h=don.h, water=wl.oxy, water_orig_idx=wl.oxy_orig_idx, + distance_aw=distance_aw, distance_dw=distance_dw, + d_angle=d_angle, w_angle=w_angle, type='first_deg', resnr=resnr, + restype=restype, reschain=reschain, + restype_l=restype_l, reschain_l=reschain_l, resnr_l=resnr_l, protisdon=False) pairings.append(contact) return filter_contacts(pairings) -def metal_complexation(metals, metal_binding_lig, metal_binding_bs): +def metal_complexation( + metals: list[MetalAtom], + metal_binding_lig: list[MetalBinding], + metal_binding_bs: list[MetalBinding], +) -> list[MetalComplex]: """Find all metal complexes between metals and appropriate groups in both protein and ligand, as well as water""" - data = namedtuple('metal_complex', 'metal metal_orig_idx metal_type target target_orig_idx target_type ' - 'coordination_num distance resnr restype ' - 'reschain restype_l reschain_l resnr_l location rms, geometry num_partners complexnum') pairings_dict = {} pairings = [] # #@todo Refactor @@ -384,7 +427,6 @@ def metal_complexation(metals, metal_binding_lig, metal_binding_bs): angles_dict[target] = angles all_total = [] # Record fit information for each geometry tested - gdata = namedtuple('gdata', 'geometry rms coordination excluded diff_targets') # Geometry Data # Can't specify geometry with only one target if num_targets == 1: final_geom = 'NA' @@ -437,8 +479,13 @@ def metal_complexation(metals, metal_binding_lig, metal_binding_bs): geometry_total = np.mean(geometry_scores) # Record the targets not used for excluding them when deciding for a final geometry [not_used.append(target) for target in angles_dict if target not in used_up_targets] - all_total.append(gdata(geometry=geometry, rms=geometry_total, coordination=coo, - excluded=not_used, diff_targets=coo_diff)) + all_total.append(GeometryFit( + geometry=geometry, + rms=geometry_total, + coordination=coo, + excluded=not_used, + diff_targets=coo_diff, + )) # Make a decision here. Starting with the geometry with lowest difference in ideal and observed partners ... # Check if the difference between the RMS to the next best solution is not larger than 0.5 @@ -469,11 +516,26 @@ def metal_complexation(metals, metal_binding_lig, metal_binding_bs): metal_orig_atom = metal_to_orig_atom[metal] restype_l, reschain_l, resnr_l = whichrestype(metal_orig_atom), whichchain( metal_orig_atom), whichresnumber(metal_orig_atom) - contact = data(metal=metal, metal_orig_idx=metal_to_id[metal], metal_type=metal.type, - target=target, target_orig_idx=target.atom_orig_idx, target_type=target.type, - coordination_num=final_coo, distance=distance, resnr=target.resnr, - restype=target.restype, reschain=target.reschain, location=target.location, - rms=rms, geometry=final_geom, num_partners=num_targets, complexnum=cnum + 1, - resnr_l=resnr_l, restype_l=restype_l, reschain_l=reschain_l) + contact = MetalComplex( + metal=metal, + metal_orig_idx=metal_to_id[metal], + metal_type=metal.type, + target=target, + target_orig_idx=target.atom_orig_idx, + target_type=target.type, + coordination_num=final_coo, + distance=distance, + resnr=target.resnr, + restype=target.restype, + reschain=target.reschain, + location=target.location, + rms=rms, + geometry=final_geom, + num_partners=num_targets, + complexnum=cnum + 1, + resnr_l=resnr_l, + restype_l=restype_l, + reschain_l=reschain_l, + ) pairings.append(contact) return filter_contacts(pairings) diff --git a/plip/structure/preparation.py b/plip/structure/preparation.py index 554dade..aface77 100644 --- a/plip/structure/preparation.py +++ b/plip/structure/preparation.py @@ -2,11 +2,11 @@ import os import re import tempfile -from collections import namedtuple from operator import itemgetter import numpy as np from openbabel import pybel +from openbabel.openbabel import OBResidue from plip.basic import config, logger from plip.basic.supplemental import centroid, tilde_expansion, tmpfile, classify_by_name @@ -15,22 +15,46 @@ from plip.basic.supplemental import read, nucleotide_linkage, sort_members_by_importance from plip.basic.supplemental import whichchain, whichrestype, whichresnumber, euclidean3d, int32_to_negative from plip.basic.supplemental import residue_belongs_to_receptor +from plip.basic.supplemental import LigandMember, Region, RegionPair from plip.structure.detection import halogen, pication, water_bridges, metal_complexation from plip.structure.detection import hydrophobic_interactions, pistacking, hbonds, saltbridge +from plip.structure.records import ( + AromaticRing, + Coordinate, + CovalentLink, + HalogenBondAcceptor, + HalogenBondDonor, + HydrogenBond, + HydrogenBondAcceptor, + HydrogenBondDonor, + HydrophobicAtom, + HydrophobicInteraction, + LigandCharge, + LigandMetalBinding, + LigandRecord, + MetalAtom, + PiCationInteraction, + PiStack, + ProteinCharge, + ProteinMetalBinding, + SaltBridge, + WaterBridge, + WaterMolecule, +) logger = logger.get_logger() class PDBParser: - def __init__(self, pdbpath, as_string): + def __init__(self, pdbpath: str, as_string: bool) -> None: self.as_string = as_string self.pdbpath = pdbpath self.num_fixed_lines = 0 - self.covlinkage = namedtuple("covlinkage", "id1 chain1 pos1 conf1 id2 chain2 pos2 conf2") + self.covlinkage = CovalentLink self.pdb_file_was_corrected = False self.proteinmap, self.modres, self.covalent, self.altconformations, self.corrected_pdb = self.parse_pdb() - def parse_pdb(self): + def parse_pdb(self) -> tuple[dict[int, int], set[str], list[CovalentLink], list[int], str]: """Extracts additional information from PDB files. I. When reading in a PDB file, OpenBabel numbers ATOMS and HETATOMS continously. In PDB files, TER records are also counted, leading to a different numbering system. @@ -130,7 +154,7 @@ def parse_pdb(self): covalent.append(self.get_linkage(line)) return d, modres, covalent, alt, corrected_pdb - def fix_pdbline(self, pdbline, lastnum): + def fix_pdbline(self, pdbline: str, lastnum: int) -> tuple[str | None, int]: """Fix a PDB line if information is missing.""" pdbqt_conversion = { "HD": "H", "HS": "H", "NA": "N", @@ -222,7 +246,7 @@ def fix_pdbline(self, pdbline, lastnum): self.num_fixed_lines += 1 if fixed else 0 return pdbline + '\n', max(new_num, lastnum) - def get_linkage(self, line): + def get_linkage(self, line: str) -> CovalentLink: """Get the linkage information from a LINK entry PDB line.""" conf1, id1, chain1, pos1 = line[16].strip(), line[17:20].strip(), line[21].strip(), int(line[22:26]) conf2, id2, chain2, pos2 = line[46].strip(), line[47:50].strip(), line[51].strip(), int(line[52:56]) @@ -231,7 +255,14 @@ def get_linkage(self, line): class LigandFinder: - def __init__(self, proteincomplex, altconf, modres, covalent, mapper): + def __init__( + self, + proteincomplex: pybel.Molecule, + altconf: list[int], + modres: set[str], + covalent: list[CovalentLink], + mapper: "Mapper", + ) -> None: self.lignames_all = None self.lignames_kept = None self.water = None @@ -243,7 +274,7 @@ def __init__(self, proteincomplex, altconf, modres, covalent, mapper): self.ligands = self.getligs() self.excluded = sorted(list(self.lignames_all.difference(set(self.lignames_kept)))) - def getpeptides(self, chain): + def getpeptides(self, chain: str) -> LigandRecord | None: """If peptide ligand chains are defined via the command line options, try to extract the underlying ligand formed by all residues in the given chain without water @@ -263,7 +294,7 @@ def getpeptides(self, chain): ligand = self.extract_ligand(non_water) return ligand - def getregion(self, ligand_region, bs_region=None): + def getregion(self, ligand_region: Region, bs_region: Region | None = None) -> LigandRecord | None: all_from_region = [] for chain, residue_numbers in ligand_region.items(): if config.KEEPMOD: @@ -292,7 +323,7 @@ def getregion(self, ligand_region, bs_region=None): ligand = self.extract_ligand(non_water, regions=(ligand_region, bs_region)) return ligand - def getligs(self): + def getligs(self) -> list[LigandRecord]: """Get all ligands from a PDB file and prepare them for analysis. Returns all non-empty ligands. """ @@ -349,9 +380,8 @@ def getligs(self): return [lig for lig in ligands if len(lig.mol.atoms) != 0] - def extract_ligand(self, kmer, regions=None): + def extract_ligand(self, kmer: list[OBResidue], regions: RegionPair | None = None) -> LigandRecord: """Extract the ligand by copying atoms and bonds and assign all information necessary for later steps.""" - data = namedtuple('ligand', 'mol hetid chain position water members longname type atomorder can_to_pdb regions') members = [(res.GetName(), res.GetChain(), int32_to_negative(res.GetNum())) for res in kmer] members = sort_members_by_importance(members) rname, rchain, rnum = members[0] @@ -424,13 +454,13 @@ def extract_ligand(self, kmer, regions=None): if atomorder is not None: can_to_pdb = {atomorder[key - 1]: mapold[key] for key in mapold} - ligand = data(mol=lig, hetid=rname, chain=rchain, position=rnum, water=self.water, - members=members, longname=longname, type=ligtype, atomorder=atomorder, - can_to_pdb=can_to_pdb, regions=regions) + ligand = LigandRecord(mol=lig, hetid=rname, chain=rchain, position=rnum, water=self.water, + members=members, longname=longname, type=ligtype, atomorder=atomorder, + can_to_pdb=can_to_pdb, regions=regions) return ligand @staticmethod - def is_het_residue(obres): + def is_het_residue(obres: OBResidue) -> bool: """Given an OBResidue, determines if the residue is indeed a possible ligand in the PDB file""" if not obres.GetResidueProperty(0): @@ -449,7 +479,7 @@ def is_het_residue(obres): return True return False - def filter_for_ligands(self): + def filter_for_ligands(self) -> tuple[list[OBResidue], set[str], list[OBResidue]]: """Given an OpenBabel Molecule, get all ligands, their names, and water""" candidates1 = [o for o in pybel.ob.OBResidueIter( @@ -481,7 +511,7 @@ def filter_for_ligands(self): return selected_ligands, all_lignames, water - def identify_kmers(self, residues): + def identify_kmers(self, residues: dict[tuple[str, str, int], OBResidue]) -> list[list[OBResidue]]: """Using the covalent linkage information, find out which fragments/subunits form a ligand.""" # Remove all those not considered by ligands and pairings including alternate conformations @@ -514,12 +544,18 @@ def identify_kmers(self, residues): class Mapper: """Provides functions for mapping atom IDs in the correct way""" - def __init__(self): + def __init__(self) -> None: self.proteinmap = None # Map internal atom IDs of protein residues to original PDB Atom IDs self.ligandmaps = {} # Map IDs of new ligand molecules to internal IDs (or PDB IDs?) self.original_structure = None - def mapid(self, idx, mtype, bsid=None, to='original'): # Mapping to original IDs is standard for ligands + def mapid( + self, + idx: int, + mtype: str, + bsid: str | None = None, + to: str = 'original', + ) -> int | None: # Mapping to original IDs is standard for ligands if mtype == 'reversed': # Needed to map internal ID back to original protein ID return self.reversed_proteinmap[idx] if mtype == 'protein': @@ -530,7 +566,7 @@ def mapid(self, idx, mtype, bsid=None, to='original'): # Mapping to original ID elif to == 'original': return self.proteinmap[self.ligandmaps[bsid][idx]] - def id_to_atom(self, idx): + def id_to_atom(self, idx: int) -> pybel.Atom: """Returns the atom for a given original ligand ID. To do this, the ID is mapped to the protein first and then the atom returned. """ @@ -539,7 +575,7 @@ def id_to_atom(self, idx): class Mol: - def __init__(self, altconf, mapper, mtype, bsid): + def __init__(self, altconf: list[int], mapper: "Mapper", mtype: str, bsid: str | None) -> None: self.mtype = mtype self.bsid = bsid self.rings = None @@ -550,10 +586,9 @@ def __init__(self, altconf, mapper, mtype, bsid): self.altconf = altconf self.Mapper = mapper - def hydrophobic_atoms(self, all_atoms): + def hydrophobic_atoms(self, all_atoms: list[pybel.Atom]) -> list[HydrophobicAtom]: """Select all carbon atoms which have only carbons and/or hydrogens as direct neighbors.""" atom_set = [] - data = namedtuple('hydrophobic', 'atom orig_atom orig_idx') atm = [a for a in all_atoms if a.atomicnum == 6 and set([natom.GetAtomicNum() for natom in pybel.ob.OBAtomAtomIter(a.OBAtom)]).issubset( {1, 6})] @@ -561,45 +596,49 @@ def hydrophobic_atoms(self, all_atoms): orig_idx = self.Mapper.mapid(atom.idx, mtype=self.mtype, bsid=self.bsid) orig_atom = self.Mapper.id_to_atom(orig_idx) if atom.idx not in self.altconf: - atom_set.append(data(atom=atom, orig_atom=orig_atom, orig_idx=orig_idx)) + atom_set.append(HydrophobicAtom(atom=atom, orig_atom=orig_atom, orig_idx=orig_idx)) return atom_set - def find_hba(self, all_atoms): + def find_hba(self, all_atoms: list[pybel.Atom]) -> list[HydrogenBondAcceptor]: """Find all possible hydrogen bond acceptors""" - data = namedtuple('hbondacceptor', 'a a_orig_atom a_orig_idx type') a_set = [] for atom in all_atoms: if atom.atomicnum not in [9, 17, 35, 53] and atom.idx not in self.altconf: # Exclude halogen atoms a_orig_idx = self.Mapper.mapid(atom.idx, mtype=self.mtype, bsid=self.bsid) a_orig_atom = self.Mapper.id_to_atom(a_orig_idx) if a_orig_atom.OBAtom.IsHbondAcceptor(): - a_set.append(data(a=atom, a_orig_atom=a_orig_atom, a_orig_idx=a_orig_idx, type='regular')) + a_set.append(HydrogenBondAcceptor( + a=atom, a_orig_atom=a_orig_atom, a_orig_idx=a_orig_idx, type='regular')) a_set = sorted(a_set, key=lambda x: x.a_orig_idx) return a_set - def find_hbd(self, all_atoms, hydroph_atoms): + def find_hbd( + self, + all_atoms: list[pybel.Atom], + hydroph_atoms: list[HydrophobicAtom], + ) -> list[HydrogenBondDonor]: """Find all possible strong and weak hydrogen bonds donors (all hydrophobic C-H pairings)""" donor_pairs = [] - data = namedtuple('hbonddonor', 'd d_orig_atom d_orig_idx h type') for donor in [a for a in all_atoms if a.OBAtom.IsHbondDonor() and a.idx not in self.altconf]: for adj_atom in [a for a in pybel.ob.OBAtomAtomIter(donor.OBAtom) if a.IsHbondDonorH()]: d_orig_idx = self.Mapper.mapid(donor.idx, mtype=self.mtype, bsid=self.bsid) d_orig_atom = self.Mapper.id_to_atom(d_orig_idx) - donor_pairs.append(data(d=donor, d_orig_atom=d_orig_atom, d_orig_idx=d_orig_idx, - h=pybel.Atom(adj_atom), type='regular')) + donor_pairs.append(HydrogenBondDonor( + d=donor, d_orig_atom=d_orig_atom, d_orig_idx=d_orig_idx, + h=pybel.Atom(adj_atom), type='regular')) for carbon in hydroph_atoms: for adj_atom in [a for a in pybel.ob.OBAtomAtomIter(carbon.atom.OBAtom) if a.GetAtomicNum() == 1]: d_orig_idx = self.Mapper.mapid(carbon.atom.idx, mtype=self.mtype, bsid=self.bsid) d_orig_atom = self.Mapper.id_to_atom(d_orig_idx) - donor_pairs.append(data(d=carbon, d_orig_atom=d_orig_atom, - d_orig_idx=d_orig_idx, h=pybel.Atom(adj_atom), type='weak')) + donor_pairs.append(HydrogenBondDonor( + d=carbon, d_orig_atom=d_orig_atom, + d_orig_idx=d_orig_idx, h=pybel.Atom(adj_atom), type='weak')) donor_pairs = sorted(donor_pairs, key=lambda x: (x.d_orig_idx, x.h.idx)) return donor_pairs - def find_rings(self, mol, all_atoms): + def find_rings(self, mol: pybel.Molecule, all_atoms: list[pybel.Atom]) -> list[AromaticRing]: """Find rings and return only aromatic. Rings have to be sufficiently planar OR be detected by OpenBabel as aromatic.""" - data = namedtuple('aromatic_ring', 'atoms orig_atoms atoms_orig_idx normal obj center type') rings = [] aromatic_amino = ['TYR', 'TRP', 'HIS', 'PHE'] ring_candidates = mol.OBMol.GetSSSR() @@ -626,49 +665,68 @@ def find_rings(self, mol, all_atoms): atoms_orig_idx = [self.Mapper.mapid(r_atom.idx, mtype=self.mtype, bsid=self.bsid) for r_atom in r_atoms] orig_atoms = [self.Mapper.id_to_atom(idx) for idx in atoms_orig_idx] - rings.append(data(atoms=r_atoms, - orig_atoms=orig_atoms, - atoms_orig_idx=atoms_orig_idx, - normal=normalize_vector(np.cross(ringv1, ringv2)), - obj=ring, - center=centroid([ra.coords for ra in r_atoms]), - type=ring_type)) + rings.append(AromaticRing(atoms=r_atoms, + orig_atoms=orig_atoms, + atoms_orig_idx=atoms_orig_idx, + normal=normalize_vector(np.cross(ringv1, ringv2)), + obj=ring, + center=centroid([ra.coords for ra in r_atoms]), + type=ring_type)) return rings - def append_func_group_to_data(self, a, a_set, data, a_orig_idx, charge_type, center, fgroup, res=None): - """Appends atoms that are part of a functional group as named tuple to the a_set""" - if not res: - if not isinstance(a, list): - a_orig = self.Mapper.id_to_atom(a_orig_idx) - a = [a, ] - a_orig = [a_orig, ] - a_orig_idx = [a_orig_idx, ] - else: - a_orig = [self.Mapper.id_to_atom(idx) for idx in a_orig_idx] - a_set.append(data(atoms=a, orig_atoms=a_orig, atoms_orig_idx=a_orig_idx, type=charge_type, - center=center, fgroup=fgroup)) - return a_set + def append_func_group_to_data( + self, + a: pybel.Atom | list[pybel.Atom], + a_set: list[ProteinCharge | LigandCharge], + a_orig_idx: int | list[int], + charge_type: str, + center: Coordinate, + fgroup: str, + res: OBResidue | None = None, + ) -> list[ProteinCharge | LigandCharge]: + """Append atoms that are part of a functional group to the corresponding charge record.""" + atoms = a if isinstance(a, list) else [a] + atom_indices = a_orig_idx if isinstance(a_orig_idx, list) else [a_orig_idx] + if res is None: + original_atoms = [self.Mapper.id_to_atom(idx) for idx in atom_indices] + a_set.append(LigandCharge( + atoms=atoms, + orig_atoms=original_atoms, + atoms_orig_idx=atom_indices, + type=charge_type, + center=center, + fgroup=fgroup, + )) else: - if not isinstance(a, list): - a = [a, ] - a_orig_idx = [a_orig_idx, ] - a_set.append(data(atoms=a, atoms_orig_idx=a_orig_idx, type=charge_type, center=center, - restype=res.GetName(), resnr=res.GetNum(), reschain=res.GetChain())) - return a_set - - def append_if_charged_func_group(self, a, a_set, data, res=None): + a_set.append(ProteinCharge( + atoms=atoms, + atoms_orig_idx=atom_indices, + type=charge_type, + center=center, + restype=res.GetName(), + resnr=res.GetNum(), + reschain=res.GetChain(), + )) + return a_set + + def append_if_charged_func_group( + self, + a: pybel.Atom, + a_set: list[ProteinCharge | LigandCharge], + res: OBResidue | None = None, + ) -> list[ProteinCharge | LigandCharge]: """Checks if atom is part of a charged functional group and appends it to a_set if True.""" a_orig_idx = self.Mapper.mapid(a.idx, mtype=self.mtype, bsid=self.bsid) if self.is_functional_group(a, 'quartamine'): - a_set = self.append_func_group_to_data(a=a, a_set=a_set, data=data, a_orig_idx=a_orig_idx, + a_set = self.append_func_group_to_data(a=a, a_set=a_set, a_orig_idx=a_orig_idx, charge_type='positive', center=list(a.coords), fgroup='quartamine', res=res) elif self.is_functional_group(a, 'tertamine'): - a_set = self.append_func_group_to_data(a=a, a_set=a_set, data=data, a_orig_idx=a_orig_idx, + a_set = self.append_func_group_to_data(a=a, a_set=a_set, a_orig_idx=a_orig_idx, charge_type='positive', center=list(a.coords), fgroup='tertamine', res=res) if self.is_functional_group(a, 'sulfonium'): - a_set = self.append_func_group_to_data(a=a, a_set=a_set, data=data, a_orig_idx=a_orig_idx, + a_set = self.append_func_group_to_data(a=a, a_set=a_set, a_orig_idx=a_orig_idx, charge_type='positive', center=list(a.coords), fgroup='sulfonium', res=res) if self.is_functional_group(a, 'phosphate'): @@ -677,7 +735,7 @@ def append_if_charged_func_group(self, a, a_set, data, res=None): [a_contributing.append(pybel.Atom(neighbor)) for neighbor in pybel.ob.OBAtomAtomIter(a.OBAtom)] [a_contributing_orig_idx.append(self.Mapper.mapid(neighbor.idx, mtype=self.mtype, bsid=self.bsid)) for neighbor in a_contributing] - a_set = self.append_func_group_to_data(a=a_contributing, a_set=a_set, data=data, + a_set = self.append_func_group_to_data(a=a_contributing, a_set=a_set, a_orig_idx=a_contributing_orig_idx, charge_type='negative', center=a.coords, fgroup='phosphate', res=res) @@ -688,7 +746,7 @@ def append_if_charged_func_group(self, a, a_set, data, res=None): neighbor.GetAtomicNum() == 8] [a_contributing_orig_idx.append(self.Mapper.mapid(neighbor.idx, mtype=self.mtype, bsid=self.bsid)) for neighbor in a_contributing] - a_set = self.append_func_group_to_data(a=a_contributing, a_set=a_set, data=data, + a_set = self.append_func_group_to_data(a=a_contributing, a_set=a_set, a_orig_idx=a_contributing_orig_idx, charge_type='negative', center=a.coords, fgroup='sulfonicacid', res=res) @@ -698,7 +756,7 @@ def append_if_charged_func_group(self, a, a_set, data, res=None): [a_contributing_orig_idx.append(self.Mapper.mapid(neighbor.idx, mtype=self.mtype, bsid=self.bsid)) for neighbor in a_contributing] [a_contributing.append(pybel.Atom(neighbor)) for neighbor in pybel.ob.OBAtomAtomIter(a.OBAtom)] - a_set = self.append_func_group_to_data(a=a_contributing, a_set=a_set, data=data, + a_set = self.append_func_group_to_data(a=a_contributing, a_set=a_set, a_orig_idx=a_contributing_orig_idx, charge_type='negative', center=a.coords, fgroup='sulfate', res=res) @@ -707,7 +765,7 @@ def append_if_charged_func_group(self, a, a_set, data, res=None): if neighbor.GetAtomicNum() == 8] a_contributing_orig_idx = [self.Mapper.mapid(neighbor.idx, mtype=self.mtype, bsid=self.bsid) for neighbor in a_contributing] - a_set = self.append_func_group_to_data(a=a_contributing, a_set=a_set, data=data, + a_set = self.append_func_group_to_data(a=a_contributing, a_set=a_set, a_orig_idx=a_contributing_orig_idx, charge_type='negative', center=centroid([a.coords for a in a_contributing]), @@ -717,32 +775,32 @@ def append_if_charged_func_group(self, a, a_set, data, res=None): if neighbor.GetAtomicNum() == 7] a_contributing_orig_idx = [self.Mapper.mapid(neighbor.idx, mtype=self.mtype, bsid=self.bsid) for neighbor in a_contributing] - a_set = self.append_func_group_to_data(a=a_contributing, a_set=a_set, data=data, + a_set = self.append_func_group_to_data(a=a_contributing, a_set=a_set, a_orig_idx=a_contributing_orig_idx, charge_type='positive', center=a.coords, fgroup='guanidine', res=res) return a_set - def get_hydrophobic_atoms(self): + def get_hydrophobic_atoms(self) -> list[HydrophobicAtom]: return self.hydroph_atoms - def get_hba(self): + def get_hba(self) -> list[HydrogenBondAcceptor]: return self.hbond_acc_atoms - def get_hbd(self): + def get_hbd(self) -> list[HydrogenBondDonor]: return [don_pair for don_pair in self.hbond_don_atom_pairs if don_pair.type == 'regular'] - def get_weak_hbd(self): + def get_weak_hbd(self) -> list[HydrogenBondDonor]: return [don_pair for don_pair in self.hbond_don_atom_pairs if don_pair.type == 'weak'] - def get_pos_charged(self): + def get_pos_charged(self) -> list[ProteinCharge | LigandCharge]: return [charge for charge in self.charged if charge.type == 'positive'] - def get_neg_charged(self): + def get_neg_charged(self) -> list[ProteinCharge | LigandCharge]: return [charge for charge in self.charged if charge.type == 'negative'] @staticmethod - def is_functional_group(atom, group): + def is_functional_group(atom: pybel.Atom, group: str) -> bool: """Given a pybel atom, look up if it belongs to a function group""" n_atoms = [a_neighbor.GetAtomicNum() for a_neighbor in pybel.ob.OBAtomAtomIter(atom.OBAtom)] @@ -788,7 +846,7 @@ def is_functional_group(atom, group): class PLInteraction: """Class to store a ligand, a protein and their interactions.""" - def __init__(self, lig_obj, bs_obj, protcomplex): + def __init__(self, lig_obj: "Ligand", bs_obj: "BindingSite", protcomplex: "PDBComplex") -> None: """Detect all interactions when initializing""" self.ligand = lig_obj self.lig_members = lig_obj.members @@ -883,7 +941,9 @@ def __init__(self, lig_obj, bs_obj, protcomplex): else: logger.info('no interactions for this ligand') - def find_unpaired_ligand(self): + def find_unpaired_ligand( + self, + ) -> tuple[list[pybel.Atom], list[pybel.Atom], list[pybel.Atom]]: """Identify unpaired functional in groups in ligands, involving H-Bond donors, acceptors, halogen bond donors. """ unpaired_hba, unpaired_hbd, unpaired_hal = [], [], [] @@ -911,7 +971,10 @@ def find_unpaired_ligand(self): return unpaired_hba, unpaired_hbd, unpaired_hal @staticmethod - def refine_hydrophobic(all_h, pistacks): + def refine_hydrophobic( + all_h: list[HydrophobicInteraction], + pistacks: list[PiStack], + ) -> list[HydrophobicInteraction]: """Apply several rules to reduce the number of hydrophobic interactions.""" sel = {} # 1. Rings interacting via stacking can't have additional hydrophobic contacts between each other. @@ -991,7 +1054,11 @@ def refine_hydrophobic(all_h, pistacks): return hydroph_final @staticmethod - def refine_hbonds_ldon(all_hbonds, salt_lneg, salt_pneg): + def refine_hbonds_ldon( + all_hbonds: list[HydrogenBond], + salt_lneg: list[SaltBridge], + salt_pneg: list[SaltBridge], + ) -> list[HydrogenBond]: """Refine selection of hydrogen bonds. Do not allow groups which already form salt bridges to form H-Bonds.""" i_set = {} for hbond in all_hbonds: @@ -1017,7 +1084,11 @@ def refine_hbonds_ldon(all_hbonds, salt_lneg, salt_pneg): return [hb[1] for hb in second_set.values()] @staticmethod - def refine_hbonds_pdon(all_hbonds, salt_lneg, salt_pneg): + def refine_hbonds_pdon( + all_hbonds: list[HydrogenBond], + salt_lneg: list[SaltBridge], + salt_pneg: list[SaltBridge], + ) -> list[HydrogenBond]: """Refine selection of hydrogen bonds. Do not allow groups which already form salt bridges to form H-Bonds with atoms of the same group. """ @@ -1045,7 +1116,10 @@ def refine_hbonds_pdon(all_hbonds, salt_lneg, salt_pneg): return [hb[1] for hb in second_set.values()] @staticmethod - def refine_pication(all_picat, stacks): + def refine_pication( + all_picat: list[PiCationInteraction], + stacks: list[PiStack], + ) -> list[PiCationInteraction]: """Just important for constellations with histidine involved. If the histidine ring is positioned in stacking position to an aromatic ring in the ligand, there is in most cases stacking and pi-cation interaction reported as histidine also carries a positive charge in the ring. For such cases, only report stacking. @@ -1067,7 +1141,11 @@ def refine_pication(all_picat, stacks): return i_set @staticmethod - def refine_water_bridges(wbridges, hbonds_ldon, hbonds_pdon): + def refine_water_bridges( + wbridges: list[WaterBridge], + hbonds_ldon: list[HydrogenBond], + hbonds_pdon: list[HydrogenBond], + ) -> list[WaterBridge]: """A donor atom already forming a hydrogen bond is not allowed to form a water bridge. Each water molecule can only be donor for two water bridges, selecting the constellation with the omega angle closest to 110 deg.""" donor_atoms_hbonds = [hb.d_orig_idx for hb in hbonds_ldon + hbonds_pdon] @@ -1106,7 +1184,16 @@ def refine_water_bridges(wbridges, hbonds_ldon, hbonds_pdon): class BindingSite(Mol): - def __init__(self, atoms, protcomplex, cclass, altconf, min_dist, mapper, regions): + def __init__( + self, + atoms: list[pybel.Atom], + protcomplex: pybel.Molecule, + cclass: "PDBComplex", + altconf: list[int], + min_dist: dict[str, tuple[float, str]], + mapper: "Mapper", + regions: RegionPair | None, + ) -> None: """Find all relevant parts which could take part in interactions""" Mol.__init__(self, altconf, mapper, mtype='protein', bsid=None) self.complex = cclass @@ -1123,9 +1210,8 @@ def __init__(self, atoms, protcomplex, cclass, altconf, min_dist, mapper, region self.halogenbond_acc = self.find_hal(self.all_atoms) self.metal_binding = self.find_metal_binding(self.full_mol) - def find_hal(self, atoms): + def find_hal(self, atoms: list[pybel.Atom]) -> list[HalogenBondAcceptor]: """Look for halogen bond acceptors (Y-{O|P|N|S}, with Y=C,P,S)""" - data = namedtuple('hal_acceptor', 'o o_orig_idx y y_orig_idx') a_set = [] # All oxygens, nitrogen, sulfurs with neighboring carbon, phosphor, nitrogen or sulfur for a in [at for at in atoms if at.atomicnum in [8, 7, 16]]: @@ -1133,13 +1219,13 @@ def find_hal(self, atoms): if len(n_atoms) == 1: # Proximal atom o_orig_idx = self.Mapper.mapid(a.idx, mtype=self.mtype, bsid=self.bsid) y_orig_idx = self.Mapper.mapid(n_atoms[0].GetIdx(), mtype=self.mtype, bsid=self.bsid) - a_set.append(data(o=a, o_orig_idx=o_orig_idx, y=pybel.Atom(n_atoms[0]), y_orig_idx=y_orig_idx)) + a_set.append(HalogenBondAcceptor( + o=a, o_orig_idx=o_orig_idx, y=pybel.Atom(n_atoms[0]), y_orig_idx=y_orig_idx)) return a_set - def find_charged(self, mol): + def find_charged(self, mol: pybel.Molecule) -> list[ProteinCharge]: """Looks for positive charges in arginine, histidine or lysine, for negative in aspartic and glutamic acid.""" """If nucleic acids are part of the receptor, looks for negative charges in phosphate backbone""" - data = namedtuple('pcharge', 'atoms atoms_orig_idx type center restype resnr reschain') a_set = [] # Iterate through all residue, exclude those in chains defined as peptides for res in [r for r in pybel.ob.OBResidueIter(mol.OBMol) if residue_belongs_to_receptor(r, self.regions)]: @@ -1155,13 +1241,13 @@ def find_charged(self, mol): a_contributing.append(pybel.Atom(a)) a_contributing_orig_idx.append(self.Mapper.mapid(a.GetIdx(), mtype='protein')) if not len(a_contributing) == 0: - a_set.append(data(atoms=a_contributing, - atoms_orig_idx=a_contributing_orig_idx, - type='positive', - center=centroid([ac.coords for ac in a_contributing]), - restype=res.GetName(), - resnr=res.GetNum(), - reschain=res.GetChain())) + a_set.append(ProteinCharge(atoms=a_contributing, + atoms_orig_idx=a_contributing_orig_idx, + type='positive', + center=centroid([ac.coords for ac in a_contributing]), + restype=res.GetName(), + resnr=res.GetNum(), + reschain=res.GetChain())) elif res.GetName() in ('GLU', 'ASP'): # Aspartic or Glutamic Acid for a in pybel.ob.OBResidueAtomIter(res): if a.GetType().startswith('O') and res.GetAtomProperty(a, 8) \ @@ -1169,13 +1255,13 @@ def find_charged(self, mol): a_contributing.append(pybel.Atom(a)) a_contributing_orig_idx.append(self.Mapper.mapid(a.GetIdx(), mtype='protein')) if not len(a_contributing) == 0: - a_set.append(data(atoms=a_contributing, - atoms_orig_idx=a_contributing_orig_idx, - type='negative', - center=centroid([ac.coords for ac in a_contributing]), - restype=res.GetName(), - resnr=res.GetNum(), - reschain=res.GetChain())) + a_set.append(ProteinCharge(atoms=a_contributing, + atoms_orig_idx=a_contributing_orig_idx, + type='negative', + center=centroid([ac.coords for ac in a_contributing]), + restype=res.GetName(), + resnr=res.GetNum(), + reschain=res.GetChain())) if res.GetName() in config.DNA + config.RNA and config.DNARECEPTOR: # nucleic acids have negative charge in sugar phosphate for a in pybel.ob.OBResidueAtomIter(res): if a.GetType().startswith('P') and res.GetAtomProperty(a, 9) \ @@ -1183,21 +1269,25 @@ def find_charged(self, mol): a_contributing.append(pybel.Atom(a)) a_contributing_orig_idx.append(self.Mapper.mapid(a.GetIdx(), mtype='protein')) if not len(a_contributing) == 0: - a_set.append(data(atoms=a_contributing, atoms_orig_idx=a_contributing_orig_idx, type='negative', - center=centroid([ac.coords for ac in a_contributing]), restype=res.GetName(), - resnr=res.GetNum(), - reschain=res.GetChain())) + a_set.append(ProteinCharge( + atoms=a_contributing, + atoms_orig_idx=a_contributing_orig_idx, + type='negative', + center=centroid([ac.coords for ac in a_contributing]), + restype=res.GetName(), + resnr=res.GetNum(), + reschain=res.GetChain(), + )) if config.KEEPMOD and res.GetName() in self.complex.modres: atom_indices = [a.GetIdx() for a in pybel.ob.OBResidueAtomIter(res)] atoms = [atm for atm in self.all_atoms if atm.idx in atom_indices] for a in atoms: - a_set = self.append_if_charged_func_group(a=a, a_set=a_set, data=data, res=res) + a_set = self.append_if_charged_func_group(a=a, a_set=a_set, res=res) return a_set - def find_metal_binding(self, mol): + def find_metal_binding(self, mol: pybel.Molecule) -> list[ProteinMetalBinding]: """Looks for atoms that could possibly be involved in chelating a metal ion. This can be any main chain oxygen atom or oxygen, nitrogen and sulfur from specific amino acids""" - data = namedtuple('metal_binding', 'atom atom_orig_idx type restype resnr reschain location') a_set = [] for res in pybel.ob.OBResidueIter(mol.OBMol): restype, reschain, resnr = res.GetName().upper(), res.GetChain(), res.GetNum() @@ -1206,37 +1296,41 @@ def find_metal_binding(self, mol): if a.GetType().startswith('O') and res.GetAtomProperty(a, 8) \ and not self.Mapper.mapid(a.GetIdx(), mtype='protein') in self.altconf: atom_orig_idx = self.Mapper.mapid(a.GetIdx(), mtype=self.mtype, bsid=self.bsid) - a_set.append(data(atom=pybel.Atom(a), atom_orig_idx=atom_orig_idx, type='O', restype=restype, - resnr=resnr, reschain=reschain, - location='protein.sidechain')) + a_set.append(ProteinMetalBinding( + atom=pybel.Atom(a), atom_orig_idx=atom_orig_idx, type='O', + restype=restype, resnr=resnr, reschain=reschain, + location='protein.sidechain')) if restype == 'HIS': # Look for nitrogen here for a in pybel.ob.OBResidueAtomIter(res): if a.GetType().startswith('N') and res.GetAtomProperty(a, 8) \ and not self.Mapper.mapid(a.GetIdx(), mtype='protein') in self.altconf: atom_orig_idx = self.Mapper.mapid(a.GetIdx(), mtype=self.mtype, bsid=self.bsid) - a_set.append(data(atom=pybel.Atom(a), atom_orig_idx=atom_orig_idx, type='N', restype=restype, - resnr=resnr, reschain=reschain, - location='protein.sidechain')) + a_set.append(ProteinMetalBinding( + atom=pybel.Atom(a), atom_orig_idx=atom_orig_idx, type='N', + restype=restype, resnr=resnr, reschain=reschain, + location='protein.sidechain')) if restype == 'CYS': # Look for sulfur here for a in pybel.ob.OBResidueAtomIter(res): if a.GetType().startswith('S') and res.GetAtomProperty(a, 8) \ and not self.Mapper.mapid(a.GetIdx(), mtype='protein') in self.altconf: atom_orig_idx = self.Mapper.mapid(a.GetIdx(), mtype=self.mtype, bsid=self.bsid) - a_set.append(data(atom=pybel.Atom(a), atom_orig_idx=atom_orig_idx, type='S', restype=restype, - resnr=resnr, reschain=reschain, - location='protein.sidechain')) + a_set.append(ProteinMetalBinding( + atom=pybel.Atom(a), atom_orig_idx=atom_orig_idx, type='S', + restype=restype, resnr=resnr, reschain=reschain, + location='protein.sidechain')) for a in pybel.ob.OBResidueAtomIter(res): # All main chain oxygens if a.GetType().startswith('O') and res.GetAtomProperty(a, 2) \ and not self.Mapper.mapid(a.GetIdx(), mtype='protein') in self.altconf and restype != 'HOH': atom_orig_idx = self.Mapper.mapid(a.GetIdx(), mtype=self.mtype, bsid=self.bsid) - a_set.append(data(atom=pybel.Atom(a), atom_orig_idx=atom_orig_idx, type='O', restype=res.GetName(), - resnr=res.GetNum(), reschain=res.GetChain(), - location='protein.mainchain')) + a_set.append(ProteinMetalBinding( + atom=pybel.Atom(a), atom_orig_idx=atom_orig_idx, type='O', + restype=res.GetName(), resnr=res.GetNum(), reschain=res.GetChain(), + location='protein.mainchain')) return a_set class Ligand(Mol): - def __init__(self, cclass, ligand): + def __init__(self, cclass: "PDBComplex", ligand: LigandRecord) -> None: altconf = cclass.altconf self.hetid, self.chain, self.position = ligand.hetid, ligand.chain, ligand.position self.bsid = ':'.join([self.hetid, self.chain, str(self.position)]) @@ -1280,15 +1374,15 @@ def __init__(self, cclass, ligand): ###### donor_pairs = [] - data = namedtuple('hbonddonor', 'd d_orig_atom d_orig_idx h type') for donor in self.all_atoms: pdbidx = self.Mapper.mapid(donor.idx, mtype='ligand', bsid=self.bsid, to='original') d = cclass.atoms[self.pdb_to_idx_mapping[pdbidx]] if d.OBAtom.IsHbondDonor(): for adj_atom in [a for a in pybel.ob.OBAtomAtomIter(d.OBAtom) if a.IsHbondDonorH()]: d_orig_atom = self.Mapper.id_to_atom(pdbidx) - donor_pairs.append(data(d=donor, d_orig_atom=d_orig_atom, d_orig_idx=pdbidx, - h=pybel.Atom(adj_atom), type='regular')) + donor_pairs.append(HydrogenBondDonor( + d=donor, d_orig_atom=d_orig_atom, d_orig_idx=pdbidx, + h=pybel.Atom(adj_atom), type='regular')) self.hbond_don_atom_pairs = donor_pairs ####### @@ -1296,7 +1390,6 @@ def __init__(self, cclass, ligand): self.centroid = centroid([a.coords for a in self.all_atoms]) self.max_dist_to_center = max((euclidean3d(self.centroid, a.coords) for a in self.all_atoms)) self.water = [] - data = namedtuple('water', 'oxy oxy_orig_idx') for hoh in ligand.water: oxy = None for at in pybel.ob.OBResidueAtomIter(hoh): @@ -1306,25 +1399,23 @@ def __init__(self, cclass, ligand): if not set([at.GetAtomicNum() for at in pybel.ob.OBResidueAtomIter(hoh)]) == {1} and oxy is not None: if euclidean3d(self.centroid, oxy.coords) < self.max_dist_to_center + config.BS_DIST: oxy_orig_idx = self.Mapper.mapid(oxy.idx, mtype='protein') - self.water.append(data(oxy=oxy, oxy_orig_idx=oxy_orig_idx)) + self.water.append(WaterMolecule(oxy=oxy, oxy_orig_idx=oxy_orig_idx)) self.halogenbond_don = self.find_hal(self.all_atoms) self.metal_binding = self.find_metal_binding(self.all_atoms, self.water) self.metals = [] - data = namedtuple('metal', 'm orig_m m_orig_idx') for a in [a for a in self.all_atoms if a.type.upper() in config.METAL_IONS]: m_orig_idx = self.Mapper.mapid(a.idx, mtype=self.mtype, bsid=self.bsid) orig_m = self.Mapper.id_to_atom(m_orig_idx) - self.metals.append(data(m=a, m_orig_idx=m_orig_idx, orig_m=orig_m)) + self.metals.append(MetalAtom(m=a, m_orig_idx=m_orig_idx, orig_m=orig_m)) self.num_hba, self.num_hbd = len(self.hbond_acc_atoms), len(self.hbond_don_atom_pairs) self.num_hal = len(self.halogenbond_don) - def get_canonical_num(self, atomnum): + def get_canonical_num(self, atomnum: int) -> int: """Converts internal atom ID into canonical atom ID. Agrees with Canonical SMILES in XML.""" return self.atomorder[atomnum - 1] - def find_hal(self, atoms): + def find_hal(self, atoms: list[pybel.Atom]) -> list[HalogenBondDonor]: """Look for halogen bond donors (X-C, with X=F, Cl, Br, I)""" - data = namedtuple('hal_donor', 'x orig_x x_orig_idx c c_orig_idx') a_set = [] for a in atoms: if self.is_functional_group(a, 'halocarbon'): @@ -1332,23 +1423,23 @@ def find_hal(self, atoms): x_orig_idx = self.Mapper.mapid(a.idx, mtype=self.mtype, bsid=self.bsid) orig_x = self.Mapper.id_to_atom(x_orig_idx) c_orig_idx = [self.Mapper.mapid(na.GetIdx(), mtype=self.mtype, bsid=self.bsid) for na in n_atoms] - a_set.append(data(x=a, orig_x=orig_x, x_orig_idx=x_orig_idx, - c=pybel.Atom(n_atoms[0]), c_orig_idx=c_orig_idx)) + a_set.append(HalogenBondDonor( + x=a, orig_x=orig_x, x_orig_idx=x_orig_idx, + c=pybel.Atom(n_atoms[0]), c_orig_idx=c_orig_idx)) if len(a_set) != 0: logger.info(f'ligand contains {len(a_set)} halogen atom(s)') return a_set - def find_charged(self, all_atoms): + def find_charged(self, all_atoms: list[pybel.Atom]) -> list[LigandCharge]: """Identify all positively charged groups in a ligand. This search is not exhaustive, as the cases can be quite diverse. The typical cases seem to be protonated amines, quaternary ammoinium and sulfonium as mentioned in 'Cation-pi interactions in ligand recognition and catalysis' (Zacharias et al., 2002)). Identify negatively charged groups in the ligand. """ - data = namedtuple('lcharge', 'atoms orig_atoms atoms_orig_idx type center fgroup') a_set = [] if not (config.INTRA or config.PEPTIDES or config.CHAINS or config.REGIONS): for a in all_atoms: - a_set = self.append_if_charged_func_group(a=a, a_set=a_set, data=data) + a_set = self.append_if_charged_func_group(a=a, a_set=a_set) else: """We have peptide/protein chain as ligand""" """Looks for positive charges in arginine, histidine or lysine, for negative in aspartic and glutamic acid.""" @@ -1365,12 +1456,14 @@ def find_charged(self, all_atoms): a_contributing.append(pybel.Atom(a)) a_contributing_orig_idx.append(self.Mapper.mapid(a.GetIdx(), mtype=self.mtype, bsid=self.bsid)) if not len(a_contributing) == 0: - a_set.append(data(atoms=a_contributing, - orig_atoms=[self.Mapper.id_to_atom(idx) for idx in a_contributing_orig_idx], - atoms_orig_idx=a_contributing_orig_idx, - type='positive', - center=centroid([ac.coords for ac in a_contributing]), - fgroup=res.GetName()+str(res.GetNum())+res.GetChain())) + a_set.append(LigandCharge( + atoms=a_contributing, + orig_atoms=[self.Mapper.id_to_atom(idx) for idx in a_contributing_orig_idx], + atoms_orig_idx=a_contributing_orig_idx, + type='positive', + center=centroid([ac.coords for ac in a_contributing]), + fgroup=res.GetName()+str(res.GetNum())+res.GetChain(), + )) elif res.GetName() in ('GLU', 'ASP'): # Aspartic or Glutamic Acid for a in pybel.ob.OBResidueAtomIter(res): if a.GetType().startswith('O') and res.GetAtomProperty(a, 8) \ @@ -1378,26 +1471,32 @@ def find_charged(self, all_atoms): a_contributing.append(pybel.Atom(a)) a_contributing_orig_idx.append(self.Mapper.mapid(a.GetIdx(), mtype=self.mtype, bsid=self.bsid)) if not len(a_contributing) == 0: - a_set.append(data(atoms=a_contributing, - orig_atoms=[self.Mapper.id_to_atom(idx) for idx in a_contributing_orig_idx], - atoms_orig_idx=a_contributing_orig_idx, - type='negative', - center=centroid([ac.coords for ac in a_contributing]), - fgroup=res.GetName()+str(res.GetNum())+res.GetChain())) + a_set.append(LigandCharge( + atoms=a_contributing, + orig_atoms=[self.Mapper.id_to_atom(idx) for idx in a_contributing_orig_idx], + atoms_orig_idx=a_contributing_orig_idx, + type='negative', + center=centroid([ac.coords for ac in a_contributing]), + fgroup=res.GetName()+str(res.GetNum())+res.GetChain(), + )) if config.KEEPMOD and res.GetName() in self.complex.modres: atom_indices = [a.GetIdx() for a in pybel.ob.OBResidueAtomIter(res)] atoms = [atm for atm in all_atoms if atm.idx in atom_indices] for a in atoms: - a_set = self.append_if_charged_func_group(a=a, a_set=a_set, data=data) + a_set = self.append_if_charged_func_group(a=a, a_set=a_set) return a_set - def find_metal_binding(self, lig_atoms, water_oxygens): + def find_metal_binding( + self, + lig_atoms: list[pybel.Atom], + water_oxygens: list[WaterMolecule], + ) -> list[LigandMetalBinding]: """Looks for atoms that could possibly be involved in binding a metal ion. This can be any water oxygen, as well as oxygen from carboxylate, phophoryl, phenolate, alcohol; nitrogen from imidazole; sulfur from thiolate. """ a_set = [] - data = namedtuple('metal_binding', 'atom orig_atom atom_orig_idx type fgroup restype resnr reschain location') + data = LigandMetalBinding for oxygen in water_oxygens: a_set.append(data(atom=oxygen.oxy, atom_orig_idx=oxygen.oxy_orig_idx, type='O', fgroup='water', restype=whichrestype(oxygen.oxy), resnr=whichresnumber(oxygen.oxy), @@ -1468,7 +1567,7 @@ class PDBComplex: such as PDB files. """ - def __init__(self): + def __init__(self) -> None: self.interaction_sets = {} # Dictionary with site identifiers as keys and object as value self.protcomplex = None self.filetype = None @@ -1486,12 +1585,12 @@ def __init__(self): self.Mapper = Mapper() self.ligands = [] - def __str__(self): + def __str__(self) -> str: formatted_lig_names = [":".join([x.hetid, x.chain, str(x.position)]) for x in self.ligands] return "Protein structure %s with ligands:\n" % (self.pymol_name) + "\n".join( [lig for lig in formatted_lig_names]) - def load_pdb(self, pdbpath, as_string=False): + def load_pdb(self, pdbpath: str, as_string: bool = False) -> None: """Loads a pdb file with protein AND ligand(s), separates and prepares them. If specified 'as_string', the input is a PDB string instead of a path.""" if as_string: @@ -1603,12 +1702,12 @@ def load_pdb(self, pdbpath, as_string=False): else: self.sourcefiles['pdbstring'] = open(pdbpath, 'r').read() - def analyze(self): + def analyze(self) -> None: """Triggers analysis of all complexes in structure""" for ligand in self.ligands: self.characterize_complex(ligand) - def characterize_complex(self, ligand): + def characterize_complex(self, ligand: LigandRecord) -> None: """Handles all basic functions for characterizing the interactions for one ligand""" single_sites = [] @@ -1669,7 +1768,11 @@ def characterize_complex(self, ligand): pli_obj = PLInteraction(lig_obj, bs_obj, self) self.interaction_sets[ligand.mol.title] = pli_obj - def exclude_ligand_modresidues(self, ligmembers, resis): + def exclude_ligand_modresidues( + self, + ligmembers: list[LigandMember], + resis: list[OBResidue], + ) -> list[OBResidue]: """If the ligand contains modified residues, exclude these from the receptor residues.""" lig_modres = [member for member in ligmembers if member[0] in self.modres] if lig_modres: @@ -1678,12 +1781,23 @@ def exclude_ligand_modresidues(self, ligmembers, resis): else: return resis - def extract_bs(self, cutoff, ligcentroid, resis, regions=None): + def extract_bs( + self, + cutoff: float, + ligcentroid: list[float], + resis: list[OBResidue], + regions: RegionPair | None = None, + ) -> list[int]: """Return list of ids from residues belonging to the binding site""" return [obres.GetIdx() for obres in resis if self.res_belongs_to_bs(obres, cutoff, ligcentroid, regions)] @staticmethod - def res_belongs_to_bs(res, cutoff, ligcentroid, regions=None): + def res_belongs_to_bs( + res: OBResidue, + cutoff: float, + ligcentroid: list[float], + regions: RegionPair | None = None, + ) -> bool: """Check for each residue if its centroid is within a certain distance to the ligand centroid. Additionally checks if a residue belongs to a chain restricted by the user (e.g. by defining a peptide chain)""" rescentroid = centroid([(atm.x(), atm.y(), atm.z()) for atm in pybel.ob.OBResidueAtomIter(res)]) @@ -1693,13 +1807,13 @@ def res_belongs_to_bs(res, cutoff, ligcentroid, regions=None): # Add restriction via chains flag return near_enough and residue_belongs_to_receptor(res, regions) - def get_atom(self, idx): + def get_atom(self, idx: int) -> pybel.Atom: return self.atoms[idx] @property - def output_path(self): + def output_path(self) -> str: return self._output_path @output_path.setter - def output_path(self, path): - self._output_path = tilde_expansion(path) \ No newline at end of file + def output_path(self, path: str) -> None: + self._output_path = tilde_expansion(path) diff --git a/plip/structure/records.py b/plip/structure/records.py new file mode 100644 index 0000000..d2250b4 --- /dev/null +++ b/plip/structure/records.py @@ -0,0 +1,292 @@ +"""Typed immutable records shared by structure preparation and detection.""" + +from typing import NamedTuple, TypeAlias + +import numpy as np +from numpy.typing import NDArray +from openbabel import pybel +from openbabel.openbabel import OBResidue, OBRing + + +Coordinate: TypeAlias = list[float] | tuple[float, float, float] | NDArray[np.floating] +LigandMember: TypeAlias = tuple[str, str, int] +Region: TypeAlias = dict[str, list[int]] +RegionPair: TypeAlias = tuple[Region, Region | None] + + +class CovalentLink(NamedTuple): + id1: str + chain1: str + pos1: int + conf1: str + id2: str + chain2: str + pos2: int + conf2: str + + +class LigandRecord(NamedTuple): + mol: pybel.Molecule + hetid: str + chain: str + position: int + water: list[OBResidue] + members: list[LigandMember] + longname: str + type: str + atomorder: list[int] | None + can_to_pdb: dict[int, int] + regions: RegionPair | None + + +class HydrophobicAtom(NamedTuple): + atom: pybel.Atom + orig_atom: pybel.Atom + orig_idx: int + + +class HydrogenBondAcceptor(NamedTuple): + a: pybel.Atom + a_orig_atom: pybel.Atom + a_orig_idx: int + type: str + + +class HydrogenBondDonor(NamedTuple): + d: pybel.Atom | HydrophobicAtom + d_orig_atom: pybel.Atom + d_orig_idx: int + h: pybel.Atom + type: str + + +class AromaticRing(NamedTuple): + atoms: list[pybel.Atom] + orig_atoms: list[pybel.Atom] + atoms_orig_idx: list[int] + normal: NDArray[np.floating] + obj: OBRing + center: Coordinate + type: str + + +class HalogenBondAcceptor(NamedTuple): + o: pybel.Atom + o_orig_idx: int + y: pybel.Atom + y_orig_idx: int + + +class ProteinCharge(NamedTuple): + atoms: list[pybel.Atom] + atoms_orig_idx: list[int] + type: str + center: Coordinate + restype: str + resnr: int + reschain: str + + +class ProteinMetalBinding(NamedTuple): + atom: pybel.Atom + atom_orig_idx: int + type: str + restype: str + resnr: int + reschain: str + location: str + + +class WaterMolecule(NamedTuple): + oxy: pybel.Atom + oxy_orig_idx: int + + +class MetalAtom(NamedTuple): + m: pybel.Atom + orig_m: pybel.Atom + m_orig_idx: int + + +class HalogenBondDonor(NamedTuple): + x: pybel.Atom + orig_x: pybel.Atom + x_orig_idx: int + c: pybel.Atom + c_orig_idx: list[int] + + +class LigandCharge(NamedTuple): + atoms: list[pybel.Atom] + orig_atoms: list[pybel.Atom] + atoms_orig_idx: list[int] + type: str + center: Coordinate + fgroup: str + + +class LigandMetalBinding(NamedTuple): + atom: pybel.Atom + orig_atom: pybel.Atom + atom_orig_idx: int + type: str + fgroup: str + restype: str + resnr: int + reschain: str + location: str + + +MetalBinding: TypeAlias = ProteinMetalBinding | LigandMetalBinding + + +class HydrophobicInteraction(NamedTuple): + bsatom: pybel.Atom + bsatom_orig_idx: int + ligatom: pybel.Atom + ligatom_orig_idx: int + distance: float + restype: str + resnr: int + reschain: str + restype_l: str + resnr_l: int + reschain_l: str + + +class HydrogenBond(NamedTuple): + a: pybel.Atom + a_orig_idx: int + d: pybel.Atom + d_orig_idx: int + h: pybel.Atom + distance_ah: float + distance_ad: float + angle: float + type: str + protisdon: bool + resnr: int + restype: str + reschain: str + resnr_l: int + restype_l: str + reschain_l: str + sidechain: bool + atype: str + dtype: str + + +class PiStack(NamedTuple): + proteinring: AromaticRing + ligandring: AromaticRing + distance: float + angle: float + offset: float + type: str + restype: str + resnr: int + reschain: str + restype_l: str + resnr_l: int + reschain_l: str + + +class PiCationInteraction(NamedTuple): + ring: AromaticRing + charge: ProteinCharge | LigandCharge + distance: float + offset: float + type: str + restype: str + resnr: int + reschain: str + restype_l: str + resnr_l: int + reschain_l: str + protcharged: bool + + +class SaltBridge(NamedTuple): + positive: ProteinCharge | LigandCharge + negative: ProteinCharge | LigandCharge + distance: float + protispos: bool + resnr: int + restype: str + reschain: str + resnr_l: int + restype_l: str + reschain_l: str + + +class HalogenBond(NamedTuple): + acc: HalogenBondAcceptor + acc_orig_idx: int + don: HalogenBondDonor + don_orig_idx: int + distance: float + don_angle: float + acc_angle: float + restype: str + resnr: int + reschain: str + restype_l: str + resnr_l: int + reschain_l: str + donortype: str + acctype: str + sidechain: bool + + +class WaterBridge(NamedTuple): + a: pybel.Atom + a_orig_idx: int + atype: str + d: pybel.Atom + d_orig_idx: int + dtype: str + h: pybel.Atom + water: pybel.Atom + water_orig_idx: int + distance_aw: float + distance_dw: float + d_angle: float + w_angle: float + type: str + resnr: int + restype: str + reschain: str + resnr_l: int + restype_l: str + reschain_l: str + protisdon: bool + + +class MetalComplex(NamedTuple): + metal: pybel.Atom + metal_orig_idx: int + metal_type: str + target: MetalBinding + target_orig_idx: int + target_type: str + coordination_num: int + distance: float + resnr: int + restype: str + reschain: str + restype_l: str + reschain_l: str + resnr_l: int + location: str + rms: float + geometry: str + num_partners: int + complexnum: int + + +class GeometryFit(NamedTuple): + geometry: str + rms: float + coordination: int + excluded: list[int] + diff_targets: int diff --git a/plip/test/test_basic_functions.py b/plip/test/test_basic_functions.py index 4cbdb25..1485659 100644 --- a/plip/test/test_basic_functions.py +++ b/plip/test/test_basic_functions.py @@ -14,12 +14,27 @@ from plip.basic.supplemental import normalize_vector, cluster_doubles, centroid # Own modules from plip.structure.preparation import PDBComplex +from plip.structure.records import CovalentLink + + +class StructureRecordTest(unittest.TestCase): + def test_records_reuse_one_named_tuple_type(self) -> None: + first = CovalentLink('CYS', 'A', 1, '', 'LIG', 'A', 2, '') + second = CovalentLink('CYS', 'B', 3, '', 'LIG', 'B', 4, '') + + self.assertIs(type(first), type(second)) + self.assertIsInstance(first, tuple) + self.assertEqual(first.id1, first[0]) + self.assertEqual( + CovalentLink.__annotations__['pos1'], + int, + ) class TestLigandSupport(unittest.TestCase): """Test for support of different ligands""" - def test_dna_rna(self): + def test_dna_rna(self) -> None: """Test if DNA and RNA is correctly processed as ligands""" tmpmol = PDBComplex() tmpmol.load_pdb('./pdb/1tf6.pdb') @@ -30,7 +45,7 @@ def test_dna_rna(self): # DNA only contains four bases self.assertEqual(ligset, {'DG', 'DC', 'DA', 'DT'}) - def test_composite_ligand_alternate_locations(self): + def test_composite_ligand_alternate_locations(self) -> None: pdb_complex = PDBComplex() pdb_complex.load_pdb('./pdb/4gql.pdb') for ligand in pdb_complex.ligands: @@ -41,7 +56,7 @@ def test_composite_ligand_alternate_locations(self): class TestMapping(unittest.TestCase): """Test""" - def test_ids(self): + def test_ids(self) -> None: """Test if the atom IDs are correctly mapped from internal to original PDB.""" tmpmol = PDBComplex() tmpmol.load_pdb('./pdb/1vsn.pdb') @@ -80,17 +95,17 @@ class GeometryTest(unittest.TestCase): """Tests for geometrical calculations in PLIP""" @staticmethod - def vector_magnitude(v): + def vector_magnitude(v: list[float]) -> float: return numpy.sqrt(sum(x ** 2 for x in v)) # noinspection PyUnusedLocal - def setUp(self): + def setUp(self) -> None: """Generate random data for the tests""" # Generate two random n-dimensional float vectors, with -100 <= n <= 100 and values 0 <= i <= 1 dim = random.randint(1, 100) self.rnd_vec = [random.uniform(-100, 100) for i in range(dim)] - def test_euclidean(self): + def test_euclidean(self) -> None: """Tests for mathematics.euclidean""" # Are the results correct? self.assertEqual(euclidean3d([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]), 0) @@ -103,7 +118,7 @@ def test_euclidean(self): # Is the output a float? self.assertIsInstance(euclidean3d([2.0, 3.0, 4.0], [2.0, 3.0, 4.0]), float) - def test_vector(self): + def test_vector(self) -> None: """Tests for mathematics.vector""" # Are the results correct? self.assertEqual(list(vector([1, 1, 1], [0, 1, 0])), [-1, 0, -1]) @@ -113,7 +128,7 @@ def test_vector(self): # Do I get 'None' if the points have different dimensions? self.assertEqual(vector([1, 1, 1], [0, 1, 0, 1]), None) - def test_vecangle(self): + def test_vecangle(self) -> None: """Tests for mathematics.vecangle""" # Are the results correct? self.assertEqual(vecangle([3, 4], [-8, 6], deg=False), numpy.radians(90.0)) @@ -122,23 +137,23 @@ def test_vecangle(self): # Correct if both vectors are equal? self.assertEqual(vecangle([3, 3], [3, 3]), 0.0) - def test_centroid(self): + def test_centroid(self) -> None: """Tests for mathematics.centroid""" # Are the results correct? self.assertEqual(centroid([[0, 0, 0], [2, 2, 2]]), [1.0, 1.0, 1.0]) self.assertEqual(centroid([[-5, 1, 2], [10, 2, 2]]), [2.5, 1.5, 2.0]) - def test_normalize_vector(self): + def test_normalize_vector(self) -> None: """Tests for mathematics.normalize_vector""" # Are the results correct? self.assertAlmostEqual(self.vector_magnitude(normalize_vector(self.rnd_vec)), 1) - def test_projection(self): + def test_projection(self) -> None: """Tests for mathematics.projection""" # Are the results correct? self.assertEqual(projection([-1, 0, 0], [3, 3, 3], [1, 1, 1]), [3, 1, 1]) - def test_cluster_doubles(self): + def test_cluster_doubles(self) -> None: """Tests for mathematics.cluster_doubles""" # Are the results correct? self.assertEqual(set(cluster_doubles([(1, 3), (4, 1), (5, 6), (7, 5)])), {(1, 3, 4), (5, 6, 7)}) diff --git a/plip/test/test_command_line.py b/plip/test/test_command_line.py index 0e77f8f..9914c3e 100644 --- a/plip/test/test_command_line.py +++ b/plip/test/test_command_line.py @@ -18,47 +18,46 @@ def setUp(self) -> None: def tearDown(self) -> None: self.tmp_dir.cleanup() - def test_empty_input_file(self): + def test_empty_input_file(self) -> None: """Input file is empty.""" exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -f ./special/empty.pdb -o {self.tmp_dir.name}', shell=True) self.assertEqual(exitcode, 1) - def test_invalid_pdb_id(self): + def test_invalid_pdb_id(self) -> None: """A PDB ID with no valid PDB record is provided.""" exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -i xx1x -o {self.tmp_dir.name}', shell=True) self.assertEqual(exitcode, 1) - def test_invalid_input_file(self): + def test_invalid_input_file(self) -> None: """A file is provided which is not a PDB file.""" exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -f ./special/non-pdb.pdb -o {self.tmp_dir.name}', shell=True) self.assertEqual(exitcode, 1) - def test_pdb_format_not_available(self): + def test_pdb_format_not_available(self) -> None: """A valid PDB ID is provided, but there is no entry in PDB format from wwPDB""" exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -i 4v59 -o {self.tmp_dir.name}', shell=True) self.assertEqual(exitcode, 1) - def test_pdb_format_available(self): + def test_pdb_format_available(self) -> None: """A valid PDB ID is provided, but there is no entry in PDB format from wwPDB""" exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -i 1acj -o {self.tmp_dir.name}', shell=True) self.assertEqual(exitcode, 0) - def test_valid_pdb(self): + def test_valid_pdb(self) -> None: """A PDB ID with no valid PDB record is provided.""" exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -x -f ./pdb/1eve.pdb -o {self.tmp_dir.name}', shell=True) self.assertEqual(len(os.listdir(self.tmp_dir.name)), 2) self.assertEqual(exitcode, 0) - def test_stdout(self): + def test_stdout(self) -> None: """A PDB ID with no valid PDB record is provided.""" exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -t -f ./pdb/1eve.pdb -O', shell=True) self.assertEqual(exitcode, 0) - def test_help(self): + def test_help(self) -> None: """A PDB ID with no valid PDB record is provided.""" exitcode = subprocess.call(f'{sys.executable} ../plipcmd.py -h', shell=True) self.assertEqual(exitcode, 0) - diff --git a/plip/test/test_hydrogen_bonds.py b/plip/test/test_hydrogen_bonds.py index 75cae6c..6b5c91e 100644 --- a/plip/test/test_hydrogen_bonds.py +++ b/plip/test/test_hydrogen_bonds.py @@ -15,21 +15,21 @@ def characterize_complex(pdb_file: str, binding_site_id: str) -> PLInteraction: class HydrogenBondTestCase(unittest.TestCase): - def test_4dst_nondeterministic_protonation(self): + def test_4dst_nondeterministic_protonation(self) -> None: config.NOHYDRO = False for i in range(0, 10): interactions = characterize_complex('./pdb/4dst.pdb', 'GCP:A:202') all_hbonds = interactions.hbonds_ldon + interactions.hbonds_pdon self.assertTrue(len(all_hbonds) == 16 or len(all_hbonds) == 17) - def test_4dst_deterministic_protonation(self): + def test_4dst_deterministic_protonation(self) -> None: config.NOHYDRO = True for i in range(0, 10): interactions = characterize_complex('./pdb/4dst_protonated.pdb', 'GCP:A:202') all_hbonds = interactions.hbonds_ldon + interactions.hbonds_pdon self.assertTrue(len(all_hbonds) == 16) - def test_no_protonation(self): + def test_no_protonation(self) -> None: config.NOHYDRO = True interactions1 = characterize_complex('./pdb/1x0n_state_1.pdb', 'DTF:A:174') self.assertEqual(len(interactions1.hbonds_ldon), 0) diff --git a/plip/test/test_intra.py b/plip/test/test_intra.py index 49af3b1..63e32a4 100644 --- a/plip/test/test_intra.py +++ b/plip/test/test_intra.py @@ -7,7 +7,7 @@ class IntraTest(unittest.TestCase): - def test_4day(self): + def test_4day(self) -> None: config.PEPTIDES = ['C'] pdb_complex = PDBComplex() pdb_complex.load_pdb('./pdb/4day.pdb') diff --git a/plip/test/test_literature_validated.py b/plip/test/test_literature_validated.py index 144ff40..68a7c4f 100644 --- a/plip/test/test_literature_validated.py +++ b/plip/test/test_literature_validated.py @@ -17,7 +17,7 @@ class LiteratureValidatedTest(unittest.TestCase): # Literature-validated cases from publication # ############################################### - def test_1eve(self): + def test_1eve(self) -> None: """Binding of anti-Alzheimer drug E2020 to acetylcholinesterase from Torpedo californica (1eve) Reference: Chakrabarti et al. Geometry of nonbonded interactions involving planar groups in proteins. (2007) """ @@ -35,7 +35,7 @@ def test_1eve(self): pication = {pication.resnr for pication in s.pication_paro} self.assertTrue({330}.issubset(pication)) - def test_1h2t(self): + def test_1h2t(self) -> None: """Binding of methylated guanosine to heterodimeric nuclear-cap binding complex (1h2t) Reference: Chakrabarti et al. Geometry of nonbonded interactions involving planar groups in proteins. (2007) """ @@ -56,7 +56,7 @@ def test_1h2t(self): saltb = {saltbridge.resnr for saltbridge in s.saltbridge_pneg} self.assertTrue({116}.issubset(saltb)) - def test_3pxf(self): + def test_3pxf(self) -> None: """Binding of ANS to CDK2 (3pxf) Reference: Betzi et al. Discovery of a potential allosteric ligand binding site in CDK2 (2012) """ @@ -86,7 +86,7 @@ def test_3pxf(self): hydroph = {hydroph.resnr for hydroph in s.hydrophobic_contacts} self.assertTrue({52, 76}.issubset(hydroph)) - def test_2reg(self): + def test_2reg(self) -> None: """Binding of choline to ChoX (2reg) Reference: Oswald et al. Crystal structures of the choline/acetylcholine substrate-binding protein ChoX from Sinorhizobium meliloti in the liganded and unliganded-closed states. (2008) @@ -105,7 +105,7 @@ def test_2reg(self): saltb = {saltbridge.resnr for saltbridge in s.saltbridge_pneg} self.assertEqual({45}, saltb) - def test_1osn(self): + def test_1osn(self) -> None: """Binding of VZV-tk to BVDU-MP (2reg) Reference: Bird et al. Crystal structures of Varicella Zoster Virus Thyrimidine Kinase. (2003) """ @@ -123,7 +123,7 @@ def test_1osn(self): hbonds = {hbond.resnr for hbond in s.hbonds_pdon} self.assertTrue({90}.issubset(hbonds)) - def test_2w0s(self): + def test_2w0s(self) -> None: """Binding of Vacc-TK to TDP (2w0s) Reference: Caillat et al. Crystal structure of poxvirus thymidylate kinase: An unexpected dimerization has implications for antiviral therapy (2008) @@ -148,7 +148,7 @@ def test_2w0s(self): saltb = {saltbridge.resnr for saltbridge in s.saltbridge_lneg} self.assertTrue({41, 93}.issubset(saltb)) - def test_1vsn(self): + def test_1vsn(self) -> None: """Binding of NFT to Cathepsin K (1vsn) Reference: Li et al. Identification of a potent and selective non-basic cathepsin K inhibitor. (2006) """ @@ -163,7 +163,7 @@ def test_1vsn(self): hbonds = {hbond.resnr for hbond in s.hbonds_pdon} self.assertTrue({66}.issubset(hbonds)) - def test_1p5e(self): + def test_1p5e(self) -> None: """Binding of TBS to CDK2(1p5e) Reference: De Moliner et al. Alternative binding modes of an inhibitor to two different kinases. (2003) """ @@ -178,7 +178,7 @@ def test_1p5e(self): halogens = {halogen.resnr for halogen in s.halogen_bonds} self.assertTrue({10, 83}.issubset(halogens)) - def test_1acj(self): + def test_1acj(self) -> None: """Binding of Tacrine (THA) to active-site gorge of acetylcholinesterase (1acj) Reference: Harel et al. Quaternary ligand binding to aromatic residues in the active-site gorge of acetylcholinesterase.. (1993) @@ -194,7 +194,7 @@ def test_1acj(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({330, 84}.issubset(pistackres)) - def test_2zoz(self): + def test_2zoz(self) -> None: """Binding of CgmR to ethidium(2z0z) Reference: Itou et al. Crystal Structures of the Multidrug Binding Repressor Corynebacterium glutamicum CgmR in Complex with Inducers and with an Operator. (2010) @@ -214,7 +214,7 @@ def test_2zoz(self): self.assertTrue({59, 88, 63, 113, 147}.issubset(hydrophobics)) self.assertTrue({59, 88, 63, 92, 113, 147}.issubset(hydrophobics)) - def test_1xdn(self): + def test_1xdn(self) -> None: """Binding of ATP to RNA editing ligase 1 (1xdn) Reference: Deng et al. High resolution crystal structure of a key editosome enzyme from Trypanosoma brucei: RNA editing ligase 1. (2004) @@ -240,7 +240,7 @@ def test_1xdn(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({209}.issubset(pistackres)) - def test_1bma(self): + def test_1bma(self) -> None: """Binding of aminimide to porcine pancreatic elastase(1bma) Reference: Peisach et al. Interaction of a Peptidomimetic Aminimide Inhibitor with Elastase. (1995) """ @@ -262,7 +262,7 @@ def test_1bma(self): waterbridges = {wb.resnr for wb in s.water_bridges} self.assertTrue(set().issubset(waterbridges)) - def test_4rao(self): + def test_4rao(self) -> None: """Binding of (4rao) Reference: Keough et al. Aza-acyclic Nucleoside Phosphonates Containing a Second Phosphonate Group As Inhibitors of the Human, Plasmodium falciparum and vivax 6‑Oxopurine Phosphoribosyltransferases @@ -285,7 +285,7 @@ def test_4rao(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({186}.issubset(pistackres)) - def test_4qnb(self): + def test_4qnb(self) -> None: """Binding of (4qnb) Reference: Bhattacharya et al. Structural basis of HIV-1 capsid recognition by PF74 and CPSF6(2014) """ @@ -303,7 +303,7 @@ def test_4qnb(self): picat = {pication.resnr for pication in s.pication_laro} self.assertEqual({70}, picat) - def test_4kya(self): + def test_4kya(self) -> None: """Binding of non-classical TS inhibitor 3 with Toxoplasma gondii TS-DHFR(4kya) Reference: Zaware et al. Structural basis of HIV-1 capsid recognition by PF74 and CPSF6(2014) """ @@ -329,7 +329,7 @@ def test_4kya(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({403, 520}.issubset(pistackres)) - def test_1n7g(self): + def test_1n7g(self) -> None: """Binding of NADPH to MURI from Arabidopsis thaliana (1n7g) Reference: Mulichak et al. Structure of the MUR1 GDP-mannose 4, 6-dehydratase from Arabidopsis thaliana: implications for ligand binding and specificity(2002) @@ -359,7 +359,7 @@ def test_1n7g(self): picat = {pication.resnr for pication in s.pication_laro} self.assertEqual({60}, picat) - def test_4alw(self): + def test_4alw(self) -> None: """Binding of benzofuropyrimidinones compound 3 to PIM-1 (4alw) Reference: Tsuhako et al. The design, synthesis, and biological evaluation of PIM kinase inhibitors.(2012) """ @@ -377,7 +377,7 @@ def test_4alw(self): saltb = {saltbridge.resnr for saltbridge in s.saltbridge_pneg} self.assertTrue({186, 171}.issubset(saltb)) - def test_3o1h(self): + def test_3o1h(self) -> None: """Binding of TMAO to TorT-TorS system(3o1h) Reference: Hendrickson et al. An Asymmetry-to-Symmetry Switch in Signal Transmission by the Histidine Kinase Receptor for TMAO.(2013) @@ -397,7 +397,7 @@ def test_3o1h(self): picat = {pication.resnr for pication in s.pication_paro} self.assertEqual({44}, picat) - def test_3thy(self): + def test_3thy(self) -> None: """Binding of ADP tp MutS(3thy) Reference: Shikha et al. Mechanism of mismatch recognition revealed by human MutSβ bound to unpaired DNA loops.(2012) """ @@ -415,7 +415,7 @@ def test_3thy(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({815}.issubset(pistackres)) - def test_3tah(self): + def test_3tah(self) -> None: """Binding of BGO to an an N11A mutant of the G-protein domain of FeoB.(3tah) Reference: Ash et al. The structure of an N11A mutant of the G-protein domain of FeoB.(2011) """ @@ -434,7 +434,7 @@ def test_3tah(self): saltb = {saltbridge.resnr for saltbridge in s.saltbridge_pneg} self.assertTrue({116}.issubset(saltb)) - def test_3r0t(self): + def test_3r0t(self) -> None: """Binding of protein kinase CK2 alpha subunit in with the inhibitor CX-5279 (3r0t) Reference: Battistutta et al. Unprecedented selectivity and structural determinants of a new class of protein kinase CK2 inhibitors in clinical trials for the treatment of cancer (2011). @@ -462,7 +462,7 @@ def test_3r0t(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({160}.issubset(pistackres)) - def test_1aku(self): + def test_1aku(self) -> None: """Binding of Flavin mononucleotido with D.Vulgaris(1aku) Reference: McCarthy et al. Crystallographic Investigation of the Role of Aspartate 95 in the Modulation of the Redox Potentials of DesulfoVibrio Vulgaris Flavodoxin.(2002) @@ -488,7 +488,7 @@ def test_1aku(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({98}.issubset(pistackres)) - def test_4pjt(self): + def test_4pjt(self) -> None: """Binding of BMN 673 to catPARP1(4pj7) Reference: Aoyagi-Scharber et al. Structural basis for the inhibition of poly(ADP-ribose) polymerases 1 and 2 by BMN 673, a potent inhibitor derived from dihydropyridophthalazinone.(2014) @@ -507,7 +507,7 @@ def test_4pjt(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({889, 907}.issubset(pistackres)) - def test_1bju(self): + def test_1bju(self) -> None: """Binding of ACPU to bovine tripsin(1bju) Reference: Presnell et al. Oxyanion-Mediated Inhibition of Serine Proteases.(1998) """ @@ -537,7 +537,7 @@ def test_1bju(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({57}.issubset(pistackres)) - def test_4agl(self): + def test_4agl(self) -> None: """Binding of P53 to PhiKan784(4agl) Reference: Wilcken et al. Halogen-Enriched Fragment Libraries as Leads for Drug Rescue of Mutant p53.(2012) """ @@ -558,7 +558,7 @@ def test_4agl(self): halogens = {halogen.resnr for halogen in s.halogen_bonds} self.assertTrue({145}.issubset(halogens)) - def test_2efj(self): + def test_2efj(self) -> None: """Binding of teobromine to 1,7 dimethylxanthine methyltransferase(2efj) Reference: McCarthy et al. The Structure of Two N-Methyltransferases from the Caffeine Biosynthetic Pathway.(2007) @@ -577,7 +577,7 @@ def test_2efj(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({157}.issubset(pistackres)) - def test_2iuz(self): + def test_2iuz(self) -> None: """Binding of C2-dicaffeine to Aspergilius fumigates(2iuz) Reference: Schüttelkopf et al. Screening-based discovery and structural dissection of a novel family 18 chitinase inhibitor.(2006) @@ -603,7 +603,7 @@ def test_2iuz(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({52, 137, 384}.issubset(pistackres)) - def test_3shy(self): + def test_3shy(self) -> None: """Binding of 5FO to PDE5A1 catalytic domain(3shy) Reference: Xu et al. Utilization of halogen bond in lead optimization: A case study of rational design of potent phosphodiesterase type 5 (PDE5) inhibitors.(2011) @@ -628,7 +628,7 @@ def test_3shy(self): halogens = {halogen.resnr for halogen in s.halogen_bonds} self.assertTrue({612}.issubset(halogens)) - def test_1ay8(self): + def test_1ay8(self) -> None: """Binding of PLP to aromatic amino acid aminotransferase(1ay8) Reference: Okamoto et al. Crystal structures of Paracoccus denitrificans aromatic amino acid aminotransferase: a substrate recognition site constructed by rearrangement of hydrogen bond network..(1998) @@ -650,7 +650,7 @@ def test_1ay8(self): pistackres = {pistack.resnr for pistack in s.pistacking} self.assertTrue({140}.issubset(pistackres)) - def test_4rdl(self): + def test_4rdl(self) -> None: """Binding of Norovirus Boxer P domain with Lewis y tetrasaccharide(4rdl) Reference: Hao et al. Crystal structures of GI.8 Boxer virus P dimers in complex with HBGAs, a novel evolutionary path selected by the Lewis epitope..(2014) @@ -676,7 +676,7 @@ def test_4rdl(self): # Additional literature-validated cases # ######################################### - def test_1hii(self): + def test_1hii(self) -> None: """HIV-2 protease in complex with novel inhibitor CGP 53820 (1hii) Reference: Comparative analysis of the X-ray structures of HIV-1 and HIV-2 proteases in complex with CGP 53820, a novel pseudosymmetric inhibitor (1995) @@ -697,7 +697,7 @@ def test_1hii(self): # #@todo Publication mentions additional possible hydrogen bond with Asp28B # Hydrogen bonds with Asp-A25 are reported as a salt bridge as both partners have (potential) charges - def test_1hvi(self): + def test_1hvi(self) -> None: """HIV-1 protease in complex with Diol inhibitor (1hvi) Reference: Influence of Stereochemistry on Activity and Binding Modes for C2 Symmetry-Based Diol Inhibitors of HIV-1 Protease (1994) @@ -723,7 +723,7 @@ def test_1hvi(self): self.assertTrue({'25B', '27A', '27B', '48A', '48B'}.issubset(hbonds)) # #@todo Paper describes additional hydrogen bond with Asp25A - def test_3o7g(self): + def test_3o7g(self) -> None: """Inhibitor PLX4032 binding to B-RAF(V600E) (3og7) Reference: Clinical efficacy of a RAF inhibitor needs broad target blockade in BRAF-mutant melanoma (2010) @@ -740,7 +740,7 @@ def test_3o7g(self): # Additional hydrogen bond to residue 530A reported self.assertTrue({'594A'}.issubset(hbonds)) - def test_1hpx(self): + def test_1hpx(self) -> None: """ HIV-1 Protease complexes with the inhibitor KNI-272 Reference: Structure of HIV-1 protease with KNI-272, a tight-binding transition-state analog diff --git a/plip/test/test_metal_coordination.py b/plip/test/test_metal_coordination.py index e5f2743..eea7925 100644 --- a/plip/test/test_metal_coordination.py +++ b/plip/test/test_metal_coordination.py @@ -17,7 +17,7 @@ class MetalCoordinationTest(unittest.TestCase): # Literature-validated cases from publication # ############################################### - def test_1rmd(self): + def test_1rmd(self) -> None: """Zinc binding sites in RAG1 dimerization domain (1rmd) Reference: Harding. The architecture of metal coordination groups in proteins. (2004), Fig. 1a """ @@ -37,7 +37,7 @@ def test_1rmd(self): self.assertEqual(s.metal_complexes[0].coordination_num, 4) self.assertEqual(s.metal_complexes[0].geometry, 'tetrahedral') - def test_1rla(self): + def test_1rla(self) -> None: """Rat liver arginase, a binuclear manganese metalloenzyme (1rmd) Reference: Harding. The architecture of metal coordination groups in proteins. (2004), Fig. 1b """ @@ -58,7 +58,7 @@ def test_1rla(self): self.assertEqual(s.metal_complexes[0].coordination_num, 5) self.assertEqual(s.metal_complexes[0].geometry, 'square.pyramidal') - def test_1het(self): + def test_1het(self) -> None: """Liver alcohol deshydrogenase (1het) Reference: Harding. The architecture of metal coordination groups in proteins. (2004), Fig. 2 """ @@ -77,7 +77,7 @@ def test_1het(self): self.assertEqual(s.metal_complexes[0].coordination_num, 4) self.assertEqual(s.metal_complexes[0].geometry, 'tetrahedral') - def test_1vfy(self): + def test_1vfy(self) -> None: """Phosphatidylinositol-3-phosphate binding FYVE domain of VPS27P protein (1vfy) Reference: Harding. The architecture of metal coordination groups in proteins. (2004), Fig. 5 """ @@ -96,7 +96,7 @@ def test_1vfy(self): self.assertEqual(s.metal_complexes[0].coordination_num, 4) self.assertEqual(s.metal_complexes[0].geometry, 'tetrahedral') - def test_2pvb(self): + def test_2pvb(self) -> None: """Pike parvalbumin binding calcium (2pvb) Reference: Harding. The architecture of metal coordination groups in proteins. (2004), Fig. 6 """ @@ -112,7 +112,7 @@ def test_2pvb(self): self.assertEqual(s.metal_complexes[0].coordination_num, 5) self.assertEqual(s.metal_complexes[0].geometry, 'square.pyramidal') - def test_2q8q(self): + def test_2q8q(self) -> None: """Crystal Structure of S. aureus IsdE complexed with heme (2q8q) Reference: Grigg et al. Heme coordination by Staphylococcus aureus IsdE. (2007) """ diff --git a/plip/test/test_pi_stacking.py b/plip/test/test_pi_stacking.py index bb560a3..ba6b37b 100644 --- a/plip/test/test_pi_stacking.py +++ b/plip/test/test_pi_stacking.py @@ -15,7 +15,7 @@ def characterize_complex(pdb_file: str, binding_site_id: str) -> PLInteraction: class RingDetectionTest(unittest.TestCase): - def test_consistent_ring_detection(self): + def test_consistent_ring_detection(self) -> None: config.NOHYDRO = True angles = set() for i in range(0, 10): diff --git a/plip/test/test_remote_services.py b/plip/test/test_remote_services.py index 0d1ee8d..4caceb1 100644 --- a/plip/test/test_remote_services.py +++ b/plip/test/test_remote_services.py @@ -14,7 +14,7 @@ class TestPDB(unittest.TestCase): """Test PDB Web Service methods""" @unittest.skip("needs re-implementation to new RCSB API standards") - def test_pdb_entry_status(self): + def test_pdb_entry_status(self) -> None: # 1a0v is an obsolete entry and is replaced by 1y46 status, current_pdbid = check_pdb_status('1a0v') self.assertEqual(status, 'OBSOLETE') diff --git a/plip/test/test_salt_bridges.py b/plip/test/test_salt_bridges.py index 21cd1cb..2c97a8c 100644 --- a/plip/test/test_salt_bridges.py +++ b/plip/test/test_salt_bridges.py @@ -14,7 +14,7 @@ def characterize_complex(pdb_file: str, binding_site_id: str) -> PLInteraction: class SaltBridgeTest(unittest.TestCase): - def test_4yb0(self): + def test_4yb0(self) -> None: '''test salt bridge detection for nucleic acids as part of the receptor''' config.DNARECEPTOR = True diff --git a/plip/test/test_structure_processing.py b/plip/test/test_structure_processing.py index 0ee216f..c12154c 100644 --- a/plip/test/test_structure_processing.py +++ b/plip/test/test_structure_processing.py @@ -14,7 +14,7 @@ def characterize_complex(pdb_file: str, binding_site_id: str) -> PLInteraction: class StructureProcessingTestCase(unittest.TestCase): - def test_nmr(self): + def test_nmr(self) -> None: all_hydrogen_bonds = set() for i in range(1, 10): config.MODEL = i @@ -24,7 +24,7 @@ def test_nmr(self): # models contain from 0-2 hydrogen bonds self.assertEqual(all_hydrogen_bonds, {0, 1, 2}) - def test_nmr_invalid_model(self): + def test_nmr_invalid_model(self) -> None: config.MODEL = 11 interactions = characterize_complex('./pdb/2ndo.pdb', 'SFQ:A:201') all_hbonds = interactions.hbonds_ldon + interactions.hbonds_pdon diff --git a/plip/test/test_water_bridges.py b/plip/test/test_water_bridges.py index 24bf128..824d65e 100644 --- a/plip/test/test_water_bridges.py +++ b/plip/test/test_water_bridges.py @@ -14,7 +14,7 @@ def characterize_complex(pdb_file: str, binding_site_id: str) -> PLInteraction: class WaterBridgeTest(unittest.TestCase): - def test_3ems(self): + def test_3ems(self) -> None: interactions = characterize_complex('./pdb/3ems.pdb', 'ARG:A:131') water_bridges = interactions.water_bridges self.assertEqual(len(water_bridges), 4) diff --git a/plip/test/test_xml_parser.py b/plip/test/test_xml_parser.py index 046a4b2..6a2af59 100644 --- a/plip/test/test_xml_parser.py +++ b/plip/test/test_xml_parser.py @@ -13,12 +13,12 @@ class XMLParserTest(unittest.TestCase): """Checks if the XML parser is working correctly""" - def setUp(self): + def setUp(self) -> None: self.px = PlipXML('./xml/1vsn.report.xml') self.bsite = self.px.bsites['NFT:A:283'] self.smiles = 'CC(C)CC(NC(c1ccc(cc1)c1ccc(cc1)S(N)(=O)=O)C(F)(F)F)C(=O)NCC=N' - def test_general_information(self): + def test_general_information(self) -> None: """Test if general information is correctly parsed.""" self.assertEqual(self.px.version, '1.4.2') self.assertEqual(self.px.pdbid, '1VSN') @@ -26,7 +26,7 @@ def test_general_information(self): self.assertEqual(self.px.filename, '1vsn.pdb') self.assertEqual(self.px.excluded, []) - def test_bsite_information(self): + def test_bsite_information(self) -> None: """Test if the binding site information is correctly parsed.""" self.assertEqual(self.bsite.pdbid, '1VSN') self.assertEqual(self.bsite.uniqueid, '1VSN:NFT:A:283') @@ -65,7 +65,7 @@ def test_bsite_information(self): # Has Interactions? self.assertTrue(self.bsite.has_interactions, True) - def test_interactions(self): + def test_interactions(self) -> None: """Test if interaction information is correctly parsed.""" # Hydrophobic Contacts diff --git a/plip/test/test_xml_writer.py b/plip/test/test_xml_writer.py index 3a28d1a..98112d8 100644 --- a/plip/test/test_xml_writer.py +++ b/plip/test/test_xml_writer.py @@ -5,7 +5,7 @@ class XMLWriterTest(unittest.TestCase): - def test_pi_stacking(self): + def test_pi_stacking(self) -> None: pdb_complex = PDBComplex() pdb_complex.load_pdb('./pdb/4dst_protonated.pdb') for ligand in pdb_complex.ligands: @@ -14,7 +14,7 @@ def test_pi_stacking(self): structure_report = StructureReport(pdb_complex, outputprefix="test_") structure_report.write_xml(as_string=True) - def test_pication(self): + def test_pication(self) -> None: pdb_complex = PDBComplex() pdb_complex.load_pdb('./pdb/6nhb.pdb') for ligand in pdb_complex.ligands: diff --git a/plip/visualization/chimera.py b/plip/visualization/chimera.py index a37fffb..e645671 100644 --- a/plip/visualization/chimera.py +++ b/plip/visualization/chimera.py @@ -1,7 +1,12 @@ +from typing import Any + +from plip.basic.remote import VisualizerData + + class ChimeraVisualizer: """Provides visualization for Chimera.""" - def __init__(self, plcomplex, chimera_module, tid): + def __init__(self, plcomplex: VisualizerData, chimera_module: Any, tid: int) -> None: self.chimera = chimera_module self.tid = tid self.uid = plcomplex.uid @@ -27,14 +32,14 @@ def __init__(self, plcomplex, chimera_module, tid): self.atoms = self.atom_by_serialnumber() - def set_initial_representations(self): + def set_initial_representations(self) -> None: """Set the initial representations""" self.update_model_dict() self.rc("background solid white") self.rc("setattr g display 0") # Hide all pseudobonds self.rc("~display #%i & :/isHet & ~:%s" % (self.model_dict[self.plipname], self.hetid)) - def update_model_dict(self): + def update_model_dict(self) -> None: """Updates the model dictionary""" dct = {} models = self.chimera.openModels @@ -42,14 +47,14 @@ def update_model_dict(self): dct[md.name] = md.id self.model_dict = dct - def atom_by_serialnumber(self): + def atom_by_serialnumber(self) -> dict[int, object]: """Provides a dictionary mapping serial numbers to their atom objects.""" atm_by_snum = {} for atom in self.model.atoms: atm_by_snum[atom.serialNumber] = atom return atm_by_snum - def show_hydrophobic(self): + def show_hydrophobic(self) -> None: """Visualizes hydrophobic contacts.""" grp = self.getPseudoBondGroup("Hydrophobic Interactions-%i" % self.tid, associateWith=[self.model]) grp.lineType = self.chimera.Dash @@ -58,7 +63,7 @@ def show_hydrophobic(self): for i in self.plcomplex.hydrophobic_contacts.pairs_ids: self.bs_res_ids.append(i[0]) - def show_hbonds(self): + def show_hbonds(self) -> None: """Visualizes hydrogen bonds.""" grp = self.getPseudoBondGroup("Hydrogen Bonds-%i" % self.tid, associateWith=[self.model]) grp.lineWidth = 3 @@ -71,7 +76,7 @@ def show_hbonds(self): b.color = self.colorbyname('blue') self.bs_res_ids.append(i[1]) - def show_halogen(self): + def show_halogen(self) -> None: """Visualizes halogen bonds.""" grp = self.getPseudoBondGroup("HalogenBonds-%i" % self.tid, associateWith=[self.model]) grp.lineWidth = 3 @@ -81,7 +86,7 @@ def show_halogen(self): self.bs_res_ids.append(i.acc_id) - def show_stacking(self): + def show_stacking(self) -> None: """Visualizes pi-stacking interactions.""" grp = self.getPseudoBondGroup("pi-Stacking-%i" % self.tid, associateWith=[self.model]) grp.lineWidth = 3 @@ -105,7 +110,7 @@ def show_stacking(self): self.bs_res_ids += stack.proteinring_atoms - def show_cationpi(self): + def show_cationpi(self) -> None: """Visualizes cation-pi interactions""" grp = self.getPseudoBondGroup("Cation-Pi-%i" % self.tid, associateWith=[self.model]) grp.lineWidth = 3 @@ -132,7 +137,7 @@ def show_cationpi(self): else: self.bs_res_ids += cat.ring_atoms - def show_sbridges(self): + def show_sbridges(self) -> None: """Visualizes salt bridges.""" # Salt Bridges grp = self.getPseudoBondGroup("Salt Bridges-%i" % self.tid, associateWith=[self.model]) @@ -160,7 +165,7 @@ def show_sbridges(self): else: self.bs_res_ids += sbridge.negative_atoms - def show_wbridges(self): + def show_wbridges(self) -> None: """Visualizes water bridges""" grp = self.getPseudoBondGroup("Water Bridges-%i" % self.tid, associateWith=[self.model]) grp.lineWidth = 3 @@ -176,7 +181,7 @@ def show_wbridges(self): else: self.bs_res_ids.append(wbridge.acc_id) - def show_metal(self): + def show_metal(self) -> None: """Visualizes metal coordination.""" grp = self.getPseudoBondGroup("Metal Coordination-%i" % self.tid, associateWith=[self.model]) grp.lineWidth = 3 @@ -190,7 +195,7 @@ def show_metal(self): if metal.location.startswith('protein'): self.bs_res_ids.append(metal.target_id) - def cleanup(self): + def cleanup(self) -> None: """Clean up the visualization.""" if not len(self.water_ids) == 0: @@ -206,11 +211,11 @@ def cleanup(self): self.rc("display :%s" % ",".join([str(self.atoms[bsid].residue.id) for bsid in self.bs_res_ids])) self.rc("color lightblue :HOH") - def zoom_to_ligand(self): + def zoom_to_ligand(self) -> None: """Centers the view on the ligand and its binding site residues.""" self.rc("center #%i & :%s" % (self.model_dict[self.plipname], self.hetid)) - def refinements(self): + def refinements(self) -> None: """Details for the visualization.""" self.rc("setattr a color gray @CENTROID") self.rc("setattr a radius 0.3 @CENTROID") diff --git a/plip/visualization/pymol.py b/plip/visualization/pymol.py index a531541..c440cfb 100644 --- a/plip/visualization/pymol.py +++ b/plip/visualization/pymol.py @@ -6,11 +6,12 @@ from pymol import cmd from plip.basic import config +from plip.basic.remote import VisualizerData class PyMOLVisualizer: - def __init__(self, plcomplex): + def __init__(self, plcomplex: VisualizerData | None) -> None: if plcomplex is not None: self.plcomplex = plcomplex self.protname = plcomplex.pdbid # Name of protein with binding site @@ -19,7 +20,7 @@ def __init__(self, plcomplex): self.ligname = "Ligand_" + self.hetid # Name of ligand self.metal_ids = plcomplex.metal_ids - def set_initial_representations(self): + def set_initial_representations(self) -> None: """General settings for PyMOL""" self.standard_settings() cmd.set('dash_gap', 0) # Show not dashes, but lines for the pliprofiler @@ -31,7 +32,7 @@ def set_initial_representations(self): cmd.clip('near', 1000) @staticmethod - def make_initial_selections(): + def make_initial_selections() -> None: """Make empty selections for structures and interactions""" for group in ['Hydrophobic-P', 'Hydrophobic-L', 'HBondDonor-P', 'HBondDonor-L', 'HBondAccept-P', 'HBondAccept-L', @@ -42,7 +43,7 @@ def make_initial_selections(): 'Unpaired-RINGS']: cmd.select(group, 'None') - def standard_settings(self): + def standard_settings(self) -> None: """Sets up standard settings for a nice visualization.""" cmd.set('bg_rgb', [1.0, 1.0, 1.0]) # White background cmd.set('depth_cue', 0) # Turn off depth cueing (no fog) @@ -53,7 +54,7 @@ def standard_settings(self): self.set_custom_colorset() @staticmethod - def set_custom_colorset(): + def set_custom_colorset() -> None: """Defines a colorset with matching colors. Provided by Joachim.""" cmd.set_color('myorange', '[253, 174, 97]') cmd.set_color('mygreen', '[171, 221, 164]') @@ -63,7 +64,7 @@ def set_custom_colorset(): cmd.set_color('mylightgreen', '[229, 245, 224]') @staticmethod - def select_by_ids(selname, idlist, selection_exists=False, chunksize=20, restrict=None): + def select_by_ids(selname: str, idlist: list[int], selection_exists: bool = False, chunksize: int = 20, restrict: str | None = None) -> None: """Selection with a large number of ids concatenated into a selection list can cause buffer overflow in PyMOL. This function takes a selection name and and list of IDs (list of integers) as input and makes a careful @@ -78,11 +79,11 @@ def select_by_ids(selname, idlist, selection_exists=False, chunksize=20, restric cmd.select(selname, '%s and %s' % (selname, restrict)) @staticmethod - def object_exists(object_name): + def object_exists(object_name: str) -> bool: """Checks if an object exists in the open PyMOL session.""" return object_name in cmd.get_names("objects") - def show_hydrophobic(self): + def show_hydrophobic(self) -> None: """Visualizes hydrophobic contacts.""" hydroph = self.plcomplex.hydrophobic_contacts if not len(hydroph.bs_ids) == 0: @@ -99,7 +100,7 @@ def show_hydrophobic(self): else: cmd.select('Hydrophobic-P', 'None') - def show_hbonds(self): + def show_hbonds(self) -> None: """Visualizes hydrogen bonds.""" hbonds = self.plcomplex.hbonds for group in [['HBondDonor-P', hbonds.prot_don_id], @@ -121,7 +122,7 @@ def show_hbonds(self): if self.object_exists('HBonds'): cmd.set('dash_color', 'blue', 'HBonds') - def show_halogen(self): + def show_halogen(self) -> None: """Visualize halogen bonds.""" halogen = self.plcomplex.halogen_bonds all_don_x, all_acc_o = [], [] @@ -138,7 +139,7 @@ def show_halogen(self): if self.object_exists('HalogenBonds'): cmd.set('dash_color', 'greencyan', 'HalogenBonds') - def show_stacking(self): + def show_stacking(self) -> None: """Visualize pi-stacking interactions.""" stacks = self.plcomplex.pistacking for i, stack in enumerate(stacks): @@ -167,7 +168,7 @@ def show_stacking(self): cmd.set('dash_gap', 0.3, 'PiStackingT') cmd.set('dash_length', 0.6, 'PiStackingT') - def show_cationpi(self): + def show_cationpi(self) -> None: """Visualize cation-pi interactions.""" for i, p in enumerate(self.plcomplex.pication): cmd.pseudoatom('ps-picat-1-%i' % i, pos=p.ring_center) @@ -192,7 +193,7 @@ def show_cationpi(self): cmd.set('dash_gap', 0.3, 'PiCation') cmd.set('dash_length', 0.6, 'PiCation') - def show_sbridges(self): + def show_sbridges(self) -> None: """Visualize salt bridges.""" for i, saltb in enumerate(self.plcomplex.saltbridges): if saltb.protispos: @@ -220,7 +221,7 @@ def show_sbridges(self): cmd.set('dash_color', 'yellow', 'Saltbridges') cmd.set('dash_gap', 0.5, 'Saltbridges') - def show_wbridges(self): + def show_wbridges(self) -> None: """Visualize water bridges.""" for bridge in self.plcomplex.waterbridges: if bridge.protisdon: @@ -243,7 +244,7 @@ def show_wbridges(self): cmd.color('lightblue', 'Water') cmd.show('spheres', 'Water') - def show_metal(self): + def show_metal(self) -> None: """Visualize metal coordination.""" metal_complexes = self.plcomplex.metal_complexes if not len(metal_complexes) == 0: @@ -268,7 +269,7 @@ def show_metal(self): cmd.show('spheres', 'Metal-W') cmd.color('lightblue', 'Metal-W') - def selections_cleanup(self): + def selections_cleanup(self) -> None: """Cleans up non-used selections""" if not len(self.plcomplex.unpaired_hba_idx) == 0: @@ -290,7 +291,7 @@ def selections_cleanup(self): cmd.delete('tmp*') cmd.delete('ps-*') - def selections_group(self): + def selections_group(self) -> None: """Group all selections""" cmd.group('Structures', '%s %s %sCartoon' % (self.protname, self.ligname, self.protname)) cmd.group('Interactions', 'Hydrophobic HBonds HalogenBonds WaterBridges PiCation PiStackingP PiStackingT ' @@ -304,7 +305,7 @@ def selections_group(self): cmd.group('Atoms.Other', 'Water Metal-W') cmd.order('*', 'y') - def additional_cleanup(self): + def additional_cleanup(self) -> None: """Cleanup of various representations""" cmd.remove('not alt ""+A') # Remove alternate conformations @@ -312,7 +313,7 @@ def additional_cleanup(self): cmd.disable('%sCartoon' % self.protname) cmd.hide('everything', 'hydrogens') - def zoom_to_ligand(self): + def zoom_to_ligand(self) -> None: """Zoom in too ligand and its interactions.""" cmd.center(self.ligname) cmd.orient(self.ligname) @@ -324,7 +325,7 @@ def zoom_to_ligand(self): cmd.zoom(self.ligname, 3) cmd.origin(self.ligname) - def save_session(self, outfolder, override=None): + def save_session(self, outfolder: str, override: str | None = None) -> None: """Saves a PyMOL session file.""" filename = '%s_%s' % (self.protname.upper(), "_".join( [self.hetid, self.plcomplex.chain, self.plcomplex.position])) @@ -333,7 +334,7 @@ def save_session(self, outfolder, override=None): cmd.save("/".join([outfolder, "%s.pse" % filename])) @staticmethod - def png_workaround(filepath, width=1200, height=800): + def png_workaround(filepath: str, width: int = 1200, height: int = 800) -> None: """Workaround for (a) severe bug(s) in PyMOL preventing ray-traced images to be produced in command-line mode. Use this function in case neither cmd.ray() or cmd.png() work. """ @@ -389,13 +390,13 @@ def png_workaround(filepath, width=1200, height=800): else: sys.stderr.write('Imagemagick not available. Images will not be resized or cropped.') - def save_picture(self, outfolder, filename): + def save_picture(self, outfolder: str, filename: str) -> None: """Saves a picture""" self.set_fancy_ray() self.png_workaround("/".join([outfolder, filename])) @staticmethod - def set_fancy_ray(): + def set_fancy_ray() -> None: """Give the molecule a flat, modern look.""" cmd.set('light_count', 6) cmd.set('spec_count', 1.5) @@ -408,7 +409,7 @@ def set_fancy_ray(): cmd.set('ambient_occlusion_mode', 1) cmd.set('ray_opaque_background', 0) # Transparent background - def adapt_for_peptides(self): + def adapt_for_peptides(self) -> None: """Adapt visualization for peptide ligands and interchain contacts""" cmd.hide('sticks', self.ligname) cmd.set('cartoon_color', 'lightorange', self.ligname) @@ -418,10 +419,10 @@ def adapt_for_peptides(self): cmd.remove('%sCartoon and chain %s' % (self.protname, self.plcomplex.chain)) cmd.set('cartoon_side_chain_helper', 0) - def adapt_for_intra(self): + def adapt_for_intra(self) -> None: """Adapt visualization for intra-protein interactions""" - def refinements(self): + def refinements(self) -> None: """Refinements for the visualization""" # Show sticks for all residues interacing with the ligand diff --git a/plip/visualization/visualize.py b/plip/visualization/visualize.py index 19fd71b..3ef71b8 100644 --- a/plip/visualization/visualize.py +++ b/plip/visualization/visualize.py @@ -1,13 +1,14 @@ from pymol import cmd from plip.basic import config, logger +from plip.basic.remote import VisualizerData from plip.basic.supplemental import start_pymol, select_region from plip.visualization.pymol import PyMOLVisualizer logger = logger.get_logger() -def visualize_in_pymol(plcomplex): +def visualize_in_pymol(plcomplex: VisualizerData) -> None: """Visualizes the given Protein-Ligand complex at one site in PyMOL.""" vis = PyMOLVisualizer(plcomplex) diff --git a/pyproject.toml b/pyproject.toml index 6f21926..0834845 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ license = "GPL-2.0-only" dynamic = ["version"] description = "PLIP - Fully automated protein-ligand interaction profiler" readme = "README.md" -requires-python = ">=3.6" +requires-python = ">=3.10" authors = [ {name = "PharmAI GmbH", email = "hello@pharm.ai"}