matteoferla/Fragmenstein

View on GitHub
fragmenstein/victor/_victor_igor.py

Summary

Maintainability
A
25 mins
Test Coverage
from ._victor_store import _VictorStore
from rdkit import Chem
from rdkit.Chem import AllChem
from ..m_rmsd import mRMSD
from typing import Dict, Optional, Callable
from ..extraction_funs import combine_for_bondorder

class _VictorIgor(_VictorStore):

    def _fix_minimized(self, ligand: Optional[Chem.Mol]=None, add_dummy:bool=True) -> Chem.Mol:
        """
        PDBs are terrible for bond order etc. and Rosetta addes these based on atom types
        igor.mol_from_pose cannot use victor.params.mol because it's upstream
        hence this method which adds to it.

        :return:
        """
        if not add_dummy:
            self.journal.debug(f'{self.long_name} - making ligand only')
        else:
            self.journal.debug(f'{self.long_name} - making ligand w/ dummy (if present)')
        if ligand is None:  # normal route
            ligand = self.igor.mol_from_pose(add_dummy=add_dummy)
        new_ligand: Chem.Mol = combine_for_bondorder(self.params.mol, ligand)
        # self.journal.warning(f'{self.long_name} - Rosetta ring closure failed: +infinity kcal/mol penalty')
        # self.energy_score['bound']['total_score'] = float('inf')
        return new_ligand

    def quick_reanimate(self) -> float:
        """
        Correct small deviations from what the forcefield likes.
        Generally flattens buckled rings and that is it.
        Reanimate is normal.

        :return:
        """
        self.igor.coordinate_constraint = 10.
        self.igor.minimize(cycles=5, default_coord_constraint=False)
        self.energy_score = self.calculate_score()
        dG_bound = self.energy_score['bound']['total_score']
        dG_unbound = self.energy_score['unbound']['total_score']
        ddG = dG_bound - dG_unbound
        return ddG

    def reanimate(self) -> float:
        """
        Calls Igor recursively until the ddG is negative or zero.
        igor.minimize does a good job. this is just to get everything as a normal molecule

        :return: ddG (kcal/mol)
        """
        ddG = 999
        self.igor.coordinate_constraint = 0.
        # self.igor.fa_intra_rep = 0.02 # 4x
        # quick unconstrained minimisation to wiggle it out of nasty local minima
        self.igor.minimize(cycles=15, default_coord_constraint=False)
        self.igor.coordinate_constraint = 2
        self.igor.minimize(cycles=5, default_coord_constraint=False)
        self.igor.coordinate_constraint = 1
        while ddG > 0:
            self.journal.debug(f'{self.long_name} - Igor minimising')
            self.igor.minimize(default_coord_constraint=False)
            self.energy_score = self.calculate_score()
            dG_bound = self.energy_score['bound']['total_score']
            dG_unbound = self.energy_score['unbound']['total_score']
            ddG = dG_bound - dG_unbound
            if ddG > 0:
                self.igor.coordinate_constraint /= 2
                self.journal.debug(
                    f'{self.long_name} - coord_constraint lowered: {self.igor.coordinate_constraint}:  {ddG} kcal/mol.')
            if self.igor.coordinate_constraint == 0.:
                self.journal.warning(f'{self.long_name} - failed to minimise without constraints:  {ddG} kcal/mol.')
                break
            elif self.igor.coordinate_constraint < 0.005:
                self.igor.coordinate_constraint = 0.
        self.ddG = ddG
        return ddG

    def reanimate_n_store(self):
        self.reanimate()
        self._store_after_reanimation()

    def _store_after_reanimation(self):
        self.minimized_pdbblock = self.igor.pose2str()
        self.post_igor_step()  # empty overridable
        # extract_mol is a classmethod in _victor_utils: it could be an option but it's inputs differ
        # and there's a few diffent details.
        self.minimized_mol = self._fix_minimized()
        self.mrmsd = self._calculate_rmsd()
        self.journal.info(f'{self.long_name} - final score: {self.ddG} kcal/mol, RMSD: {self.mrmsd.mrmsd}.')
        self._checkpoint_charlie()
        self.journal.debug(f'{self.long_name} - Completed')

    def _calculate_rmsd(self):
        self.journal.debug(f'{self.long_name} - calculating mRMSD')
        return mRMSD.from_other_annotated_mols(self.minimized_mol, self.hits, self.monster.positioned_mol)

    def calculate_score(self):
        return {**self.igor.ligand_score(),
                'unbound': self.igor.detailed_scores(self.unbound_pose, 1)}

    @property
    def constrained_atoms(self) -> int:
        """
        Do note that the whole Origins list contains hydrogens. So do not divided by len!
        :return:
        """
        try:
            origins = self.monster.origin_from_mol(self.monster.positioned_mol)
            conn = sum([len(o) > 0 and atom.GetSymbol() != 'H' for o, atom in
                          zip(origins, self.monster.positioned_mol.GetAtoms())])
        except KeyboardInterrupt as err:
            raise err
        except Exception as err:
            self.journal.warning(f'{self.long_name} - {err.__class__.__name__}: {err}')
            conn = float('nan')
        return conn

    @property
    def unconstrained_heavy_atoms(self) -> int:
        try:
            origins = self.monster.origin_from_mol(self.monster.positioned_mol)
            unconn = sum([len(o) == 0 and atom.GetSymbol() != 'H' for o, atom in
                          zip(origins, self.monster.positioned_mol.GetAtoms())])
        except KeyboardInterrupt as err:
            raise err
        except Exception as err:
            self.journal.warning(f'{self.long_name} - {err.__class__.__name__}: {err}')
            unconn = float('nan')
        return unconn