Exemple #1
0
 def from_mol(self) -> dict:
     if self.get('extension') in ('mol', 'sdf', 'mdl'):
         mol = Chem.MolFromMolBlock(self.get('block'),
                                    sanitize=True,
                                    removeHs=False,
                                    strictParsing=True)
     elif self.get('extension') in ('mol2', ):
         mol = Chem.MolFromMol2Block(self.get('block'),
                                     sanitize=True,
                                     removeHs=False)
     elif self.get('extension') in ('pdb', ):
         mol = Chem.MolFromPDBBlock(self.get('block'),
                                    sanitize=True,
                                    removeHs=False,
                                    proximityBonding=False)
     else:
         raise exc.HTTPClientError(
             f"Format {self.get('extension')} not supported")
     if self.get_bool('protons') is True:
         mol = AllChem.AddHs(mol)
     p = Params.from_mol(mol,
                         self.name,
                         generic=self.generic,
                         atomnames=self.atomnames)
     return self.to_dict(p)
Exemple #2
0
 def from_smiles(self) -> dict:
     smiles = self.get('smiles').strip()
     p = Params.from_smiles(smiles,
                            self.name,
                            generic=self.generic,
                            atomnames=self.atomnames)
     return self.to_dict(p)
Exemple #3
0
 def _calculate_combination(self):
     attachment = self._get_attachment_from_pdbblock(
     ) if self.is_covalent else None
     self._harmonise_warhead_combine()
     # TODO Does combine not need attachment??
     self.monster.modifications = self.modifications
     self.monster.combine(
         keep_all=self.monster_throw_on_discard,
         collapse_rings=True,
         joining_cutoff=self.joining_cutoff  # Å
     )
     self.mol = self.monster.positioned_mol
     self.smiles = Chem.MolToSmiles(self.mol)
     # making folder.
     self.make_output_folder()
     # paramterise
     self.journal.debug(f'{self.long_name} - Starting parameterisation')
     self.params = Params.load_mol(self.mol, name=self.ligand_resn)
     self.params.NAME = self.ligand_resn  # force it.
     self.params.polish_mol()
     # get constraint
     self.constraint = self._get_constraint(self.extra_constraint)
     self.constraint.custom_constraint += self.make_coordinate_constraints_for_unnovels(
     )
     # _get_constraint will have changed the names in params.mol so the others need changing too!
     # namely  self.params.rename_by_substructure happend.
     self.mol = Chem.Mol(self.params.mol)
     self.monster.positioned_mol = Chem.Mol(self.mol)
     # those Hs lack correct names and charge!!
     self.params.add_Hs()
     self.params.convert_mol()
     # self.journal.warning(f'{self.long_name} - CHI HAS BEEN DISABLED')
     # self.params.CHI.data = []  # TODO check if chi fix is okay
     self._log_warnings()
     self.post_params_step()  # empty overridable
     self.mmerging_mode = 'full'
     self.unminimised_pdbblock = self._plonk_monster_in_structure()
     params_file, holo_file, constraint_file = self._save_prerequisites()
     self.unbound_pose = self.params.test()
     self._checkpoint_alpha()
     self._checkpoint_bravo()
     self.igor = Igor.from_pdbblock(pdbblock=self.unminimised_pdbblock,
                                    params_file=params_file,
                                    constraint_file=constraint_file,
                                    ligand_residue=self.ligand_resi,
                                    key_residues=[self.covalent_resi])
     # user custom code.
     if self.pose_fx is not None:
         self.journal.debug(f'{self.long_name} - running custom pose mod.')
         self.pose_fx(self.igor.pose)
     else:
         self.pose_mod_step()  # empty overridable
     # storing a roundtrip
     self.unminimised_pdbblock = self.igor.pose2str()
     # minimise until the ddG is negative.
     self.reanimate_n_store()
     self.journal.debug(f'{self.long_name} - Completed')
Exemple #4
0
 def test_round(self):
     p = Params.load('example/official_PHE.params')
     p.IO_STRING[0].name3 = 'PHX'
     p.IO_STRING[0].name1 = 'Z'
     p.AA = 'UNK'  # If it's not one of the twenty (plus extras), UNK!
     del p.ROTAMER_AA[0]
     p.rename_atom(' CB ', ' CX ')  # this renames
     p.dump('fake.params')
     pose = p.test()
     buffer = pyrosetta.rosetta.std.stringbuf()
     pose.dump_pdb(pyrosetta.rosetta.std.ostream(buffer))
     pdbblock = buffer.str()
     self.assertIsNotNone(Chem.MolFromPDBBlock(pdbblock))
Exemple #5
0
 def from_pdb(self) -> dict:
     smiles = self.get('smiles').strip()
     block = self.get('block').strip()
     if smiles != '':
         p = Params.from_smiles_w_pdbblock(pdb_block=block,
                                           smiles=smiles,
                                           generic=self.generic,
                                           name=self.name,
                                           proximityBonding=False)
         return self.to_dict(p)
     else:
         protein = Chem.MolFromPDBBlock(block,
                                        removeHs=False,
                                        proximityBonding=False)
         mol = Chem.SplitMolByPDBResidues(protein,
                                          whiteList=[self.name])[self.name]
         AllChem.SanitizeMol(mol)
         mol = AllChem.AddHs(mol)
         p = Params.from_mol(mol,
                             self.name,
                             generic=self.generic,
                             atomnames=self.atomnames)
         return self.to_dict(p)
Exemple #6
0
    def _analyse(self) -> None:
        """
        This is the actual core of the class.

        :return:
        """
        # check they are okay
        if '*' in self.smiles and (self.covalent_resi is None
                                   or self.covalent_resn is None):
            raise ValueError(
                f'{self.long_name} - is covalent but without known covalent residues'
            )
            # TODO '*' in self.smiles is bad. user might start with a mol file.
        elif '*' in self.smiles:
            self.is_covalent = True
        else:
            self.is_covalent = False
        self._assert_inputs()
        # ***** PARAMS & CONSTRAINT *******
        self.journal.info(f'{self.long_name} - Starting work')
        self._log_warnings()
        # making folder.
        self._make_output_folder()
        # make params
        self.journal.debug(f'{self.long_name} - Starting parameterisation')
        self.params = Params.from_smiles(self.smiles,
                                         name=self.ligand_resn,
                                         generic=False,
                                         atomnames=self.atomnames)
        self.journal.warning(f'{self.long_name} - CHI HAS BEEN DISABLED')
        self.params.CHI.data = []  # TODO fix chi
        self.mol = self.params.mol
        self._log_warnings()
        # get constraint
        self.constraint = self._get_constraint(self.extra_constraint)
        attachment = self._get_attachment_from_pdbblock(
        ) if self.is_covalent else None
        self._log_warnings()
        self.post_params_step()
        # ***** FRAGMENSTEIN *******
        # make fragmenstein
        self.journal.debug(f'{self.long_name} - Starting fragmenstein')
        self.fragmenstein = Fragmenstein(
            mol=self.mol,
            hits=self.hits,
            attachment=attachment,
            merging_mode=self.fragmenstein_merging_mode,
            debug_draw=self.fragmenstein_debug_draw,
            average_position=self.fragmenstein_average_position)
        self.journal.debug(
            f'{self.long_name} - Tried {len(self.fragmenstein.scaffold_options)} combinations'
        )
        self.unminimised_pdbblock = self._place_fragmenstein()
        self.constraint.custom_constraint += self._make_coordinate_constraints(
        )
        self._checkpoint_bravo()
        # save stuff
        params_file, holo_file, constraint_file = self._save_prerequisites()
        self.post_fragmenstein_step()
        self.unbound_pose = self.params.test()
        self._checkpoint_alpha()
        # ***** EGOR *******
        self.journal.debug(f'{self.long_name} - setting up Igor')
        self.igor = Igor.from_pdbblock(pdbblock=self.unminimised_pdbblock,
                                       params_file=params_file,
                                       constraint_file=constraint_file,
                                       ligand_residue=self.ligand_resi,
                                       key_residues=[self.covalent_resi])
        # user custom code.
        if self.pose_fx is not None:
            self.journal.debug(f'{self.long_name} - running custom pose mod.')
            self.pose_fx(self.igor.pose)
        else:
            self.pose_mod_step()
        # storing a roundtrip
        self.unminimised_pdbblock = self.igor.pose2str()
        # minimise until the ddG is negative.
        ddG = self.reanimate()
        self.minimised_pdbblock = self.igor.pose2str()
        self.post_igor_step()
        self.minimised_mol = self._fix_minimised()
        self.mrmsd = self._calculate_rmsd()
        self.journal.info(
            f'{self.long_name} - final score: {ddG} kcal/mol {self.mrmsd.mrmsd}.'
        )
        self._checkpoint_charlie()
        self.journal.debug(f'{self.long_name} - Completed')
Exemple #7
0
    def _calculate_placement(self):
        """
        This does all the work

        :return:
        """
        # check they are okay
        self._assert_placement_inputs()
        # ***** PARAMS & CONSTRAINT *******
        self.journal.info(f'{self.long_name} - Starting work')
        self._log_warnings()
        # making folder.
        self.make_output_folder()
        # make params
        self.journal.debug(f'{self.long_name} - Starting parameterisation')
        self.params = Params.from_smiles(self.smiles,
                                         name=self.ligand_resn,
                                         generic=False,
                                         atomnames=self.atomnames)
        # self.journal.warning(f'{self.long_name} - CHI HAS BEEN DISABLED')
        # self.params.CHI.data = []  # Chi is fixed, but older version. should probably check version
        self.mol = self.params.mol
        self._log_warnings()
        # get constraint
        self.constraint = self._get_constraint(self.extra_constraint)
        attachment = self._get_attachment_from_pdbblock(
        ) if self.is_covalent else None
        self._log_warnings()
        self.post_params_step()  # empty overridable
        # ***** FRAGMENSTEIN Monster *******
        # make monster
        self.journal.debug(f'{self.long_name} - Starting fragmenstein')
        # monster_throw_on_discard controls if disconnected.
        self.monster.place(mol=self.mol,
                           attachment=attachment,
                           merging_mode=self.merging_mode)
        self.journal.debug(
            f'{self.long_name} - Tried {len(self.monster.mol_options)} combinations'
        )
        self.unminimised_pdbblock = self._plonk_monster_in_structure()
        self.constraint.custom_constraint += self.make_coordinate_constraints()
        self._checkpoint_bravo()
        # save stuff
        params_file, holo_file, constraint_file = self._save_prerequisites()
        self.post_monster_step()  # empty overridable
        self.unbound_pose = self.params.test()
        self._checkpoint_alpha()
        # ***** EGOR *******
        self.journal.debug(f'{self.long_name} - setting up Igor')
        self.igor = Igor.from_pdbblock(pdbblock=self.unminimised_pdbblock,
                                       params_file=params_file,
                                       constraint_file=constraint_file,
                                       ligand_residue=self.ligand_resi,
                                       key_residues=[self.covalent_resi])
        # user custom code.
        if self.pose_fx is not None:
            self.journal.debug(f'{self.long_name} - running custom pose mod.')
            self.pose_fx(self.igor.pose)
        else:
            self.pose_mod_step()  # empty overridable
        # storing a roundtrip
        self.unminimised_pdbblock = self.igor.pose2str()
        # minimise until the ddG is negative.
        if self.quick_renanimation:
            ddG = self.quick_reanimate()
        else:
            ddG = self.reanimate()
        self.ddG = ddG
        self._store_after_reanimation()
 def _combine_main(self):
     attachment = self._get_attachment_from_pdbblock(
     ) if self.is_covalent else None
     self.fragmenstein = Fragmenstein(
         mol=Chem.MolFromSmiles('*') if self.is_covalent else Chem.Mol(),
         hits=[],
         attachment=attachment,
         merging_mode='off')
     # collapse hits
     # fragmenstein_throw_on_discard controls if disconnected.
     self.fragmenstein.throw_on_disconnect = self.fragmenstein_throw_on_discard
     self.fragmenstein.joining_cutoff = self.fragmenstein_joining_cutoff
     self.fragmenstein.hits = [
         self.fragmenstein.collapse_ring(h) for h in self.hits
     ]
     # merge!
     self.fragmenstein.scaffold = self.fragmenstein.merge_hits()
     self._log_warnings()
     ## Discard can happen for other reasons than disconnect
     if self.fragmenstein_throw_on_discard and len(
             self.fragmenstein.unmatched):
         raise ConnectionError(f'{self.long_name} - Could not combine with {self.fragmenstein.unmatched} '+\
                               f'(>{self.fragmenstein.joining_cutoff}')
     # expand and fix
     self._log_warnings()
     self.journal.debug(f'{self.long_name} - Merged')
     self.fragmenstein.positioned_mol = self.fragmenstein.expand_ring(
         self.fragmenstein.scaffold, bonded_as_original=False)
     self._log_warnings()
     self.journal.debug(f'{self.long_name} - Expanded')
     self.fragmenstein.positioned_mol = Rectifier(
         self.fragmenstein.positioned_mol).mol
     self._log_warnings()
     # the origins are obscured because of the collapsing...
     self.fragmenstein.guess_origins(self.fragmenstein.positioned_mol,
                                     self.hits)
     self.fragmenstein.positioned_mol.SetProp('_Name', self.long_name)
     self.mol = self.fragmenstein.positioned_mol
     self.journal.debug(f'{self.long_name} - Rectified')
     self.smiles = Chem.MolToSmiles(self.mol)
     if self.fragmenstein_debug_draw:
         picture = Chem.CombineMols(
             Chem.CombineMols(self.hits[0], self.hits[1]),
             self.fragmenstein.positioned_mol)
         AllChem.Compute2DCoords(picture)
         self.fragmenstein.draw_nicely(picture)
     # making folder.
     self._make_output_folder()
     # paramterise
     self.journal.debug(f'{self.long_name} - Starting parameterisation')
     self.params = Params.load_mol(self.mol, name=self.ligand_resn)
     self.params.NAME = self.ligand_resn  # force it.
     self.params.polish_mol()
     # get constraint
     self.constraint = self._get_constraint(self.extra_constraint)
     self.constraint.custom_constraint += self._make_coordinate_constraints_for_unnovels(
     )
     # _get_constraint will have changed the names in params.mol so the others need changing too!
     # namely  self.params.rename_by_substructure happend.
     self.mol = Chem.Mol(self.params.mol)
     self.fragmenstein.positioned_mol = Chem.Mol(self.mol)
     # those Hs lack correct names and charge!!
     self.params.add_Hs()
     self.params.convert_mol()
     self.journal.warning(f'{self.long_name} - CHI HAS BEEN DISABLED')
     self.params.CHI.data = []  # TODO check if chi fix is okay
     self._log_warnings()
     self.post_params_step()
     self.fragmenstein_merging_mode = 'full'
     self.unminimised_pdbblock = self._place_fragmenstein()
     params_file, holo_file, constraint_file = self._save_prerequisites()
     self.unbound_pose = self.params.test()
     self._checkpoint_alpha()
     self._checkpoint_bravo()
     self.igor = Igor.from_pdbblock(pdbblock=self.unminimised_pdbblock,
                                    params_file=params_file,
                                    constraint_file=constraint_file,
                                    ligand_residue=self.ligand_resi,
                                    key_residues=[self.covalent_resi])
     # user custom code.
     if self.pose_fx is not None:
         self.journal.debug(f'{self.long_name} - running custom pose mod.')
         self.pose_fx(self.igor.pose)
     else:
         self.pose_mod_step()
     # storing a roundtrip
     self.unminimised_pdbblock = self.igor.pose2str()
     # minimise until the ddG is negative.
     self.reanimate_n_store()
     self.journal.debug(f'{self.long_name} - Completed')
    def from_files(cls, folder: str) -> _VictorBaseMixin:
        """
        This creates an instance form the output files. Likely to be unstable.
        Assumes the checkpoints were not altered.
        And is basically for analysis only.

        :param folder: path
        :return:
        """
        cls.journal.warning('`from_files`: You really should not use this.')
        if os.path.exists(folder):
            pass # folder is fine
        elif not os.path.exists(folder) and os.path.exists(os.path.join(cls.work_path, folder)):
            folder = os.path.join(cls.work_path, folder)
        else:
            raise FileNotFoundError(f'Folder {folder} does not exist.')
        self = cls.__new__(cls)
        self.tick = float('nan')
        self.tock = float('nan')
        self.ligand_resn = ''
        self.ligand_resi = ''
        self.covalent_resn = ''
        self.covalent_resi = ''
        self.hits = []
        self.long_name = os.path.split(folder)[1]
        paramsfiles = os.path.join(folder, f'{self.long_name}.params')
        paramstemp = os.path.join(folder, f'{self.long_name}.params_template.mol')
        if os.path.exists(paramsfiles):
            self.params = Params().load(paramsfiles)
            self.unbound_pose = self.params.test()
            if os.path.exists(paramstemp):
                self.params.mol = Chem.MolFromMolFile(paramstemp, removeHs=False)
        else:
            self.params = None
        posmol = os.path.join(folder, f'{self.long_name}.positioned.mol')
        if os.path.exists(posmol):
            self.mol = Chem.MolFromMolFile(posmol, sanitize=False, removeHs=False)
        else:
            self.journal.info(f'{self.long_name} - no positioned mol')
            self.mol = None
        fragjson = os.path.join(folder, f'{self.long_name}.fragmenstein.json')
        if os.path.exists(fragjson):
            fd = json.load(open(fragjson))
            self.smiles = fd['smiles']
            self.is_covalent = True if '*' in self.smiles else False
            self.fragmenstein = Fragmenstein(mol=self.mol,
                                             hits=self.hits,
                                             attachment=None,
                                             merging_mode='off',
                                             average_position=self.fragmenstein_average_position
                                             )
            self.fragmenstein.positioned_mol = self.mol
            self.fragmenstein.positioned_mol.SetProp('_Origins', json.dumps(fd['origin']))

        else:
            self.is_covalent = None
            self.smiles = ''
            self.fragmenstein = None
            self.journal.info(f'{self.long_name} - no fragmenstein json')
            self.N_constrained_atoms = float('nan')

        #
        self.apo_pdbblock = None
        #
        self.atomnames = None
        #
        self.extra_constraint = ''
        self.pose_fx = None
        # these are calculated
        self.constraint = None
        self.unminimised_pdbblock = None
        self.igor = None
        self.minimised_pdbblock = None
        # buffers etc.
        self._warned = []
        minjson = os.path.join(folder, f'{self.long_name}.minimised.json')
        self.mrmsd = mRSMD.mock()
        if os.path.exists(minjson):
            md = json.load(open(minjson))
            self.energy_score = md["Energy"]
            self.mrmsd.mrmsd = md["mRMSD"]
            self.mrmsd.rmsds = md["RMSDs"]
            self.igor = Igor.from_pdbfile(
                pdbfile=os.path.join(self.work_path, self.long_name, self.long_name + '.holo_minimised.pdb'),
                params_file=os.path.join(self.work_path, self.long_name, self.long_name + '.params'),
                constraint_file=os.path.join(self.work_path, self.long_name, self.long_name + '.con'))
        else:
            self.energy_score = {'ligand_ref2015': {'total_score': float('nan')},
                                 'unbound_ref2015': {'total_score': float('nan')}}

            self.journal.info(f'{self.long_name} - no min json')
        minmol = os.path.join(folder, f'{self.long_name}.minimised.mol')
        if os.path.exists(minmol):
            self.minimised_mol = Chem.MolFromMolFile(minmol, sanitize=False, removeHs=False)
        else:
            self.minimised_mol = None
        return self
class _VictorUtilsMixin(_VictorBaseMixin):

    def dock(self) -> Chem.Mol:
        """
        The docking is done by ``igor.dock()``. This basically does that, extacts ligand, saves etc.

        :return:
        """
        docked = self.igor.dock()
        self.docked_pose = docked
        docked.dump_pdb(f'{self.work_path}/{self.long_name}/{self.long_name}.holo_docked.pdb')
        ligand = self.igor.mol_from_pose(docked)
        template = AllChem.DeleteSubstructs(self.params.mol, Chem.MolFromSmiles('*'))
        lig_chem = AllChem.AssignBondOrdersFromTemplate(template, ligand)
        lig_chem.SetProp('_Name', 'docked')
        Chem.MolToMolFile(lig_chem, f'{self.work_path}/{self.long_name}/{self.long_name}.docked.mol')
        return lig_chem
        # print(pyrosetta.get_fa_scorefxn()(docked) - v.energy_score['unbound_ref2015']['total_score'])

    def summarise(self):
        if self.error:
            if self.fragmenstein is None:
                N_constrained_atoms = float('nan')
                N_unconstrained_atoms = float('nan')
            elif self.fragmenstein.positioned_mol is None:
                N_constrained_atoms = float('nan')
                N_unconstrained_atoms = float('nan')
            else:
                N_constrained_atoms = self.constrained_atoms
                N_unconstrained_atoms = self.unconstrained_heavy_atoms
            return {'name': self.long_name,
                    'smiles': self.smiles,
                    'error': self.error,
                    'mode': self.fragmenstein_merging_mode,
                    '∆∆G': float('nan'),
                    '∆G_bound': float('nan'),
                    '∆G_unbound': float('nan'),
                    'comRMSD': float('nan'),
                    'N_constrained_atoms': N_constrained_atoms,
                    'N_unconstrained_atoms': N_unconstrained_atoms,
                    'runtime': self.tock - self.tick,
                    'regarded': [h.GetProp('_Name') for h in self.hits if
                                 h.GetProp('_Name') not in self.fragmenstein.unmatched],
                    'disregarded': self.fragmenstein.unmatched
                    }
        else:
            return {'name': self.long_name,
                    'smiles': self.smiles,
                    'error': self.error,
                    'mode': self.fragmenstein_merging_mode,
                    '∆∆G': self.energy_score['ligand_ref2015']['total_score'] - \
                           self.energy_score['unbound_ref2015']['total_score'],
                    '∆G_bound': self.energy_score['ligand_ref2015']['total_score'],
                    '∆G_unbound': self.energy_score['unbound_ref2015']['total_score'],
                    'comRMSD': self.mrmsd.mrmsd,
                    'N_constrained_atoms': self.constrained_atoms,
                    'N_unconstrained_atoms': self.unconstrained_heavy_atoms,
                    'runtime': self.tock - self.tick,
                    'regarded': [h.GetProp('_Name') for h in self.hits if
                                 h.GetProp('_Name') not in self.fragmenstein.unmatched],
                    'disregarded': self.fragmenstein.unmatched
                    }

    # =================== Logging ======================================================================================

    @classmethod
    def enable_stdout(cls, level=logging.INFO) -> None:
        """
        The ``cls.journal`` is output to the terminal.
        Running it twice can be used to change level.

        :param level: logging level
        :return: None
        """
        cls.journal.handlers = [h for h in cls.journal.handlers if h.name != 'stdout']
        handler = logging.StreamHandler(sys.stdout)
        handler.setLevel(level)
        handler.set_name('stdout')
        handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s - %(message)s'))
        cls.journal.addHandler(handler)
        # logging.getLogger('py.warnings').addHandler(handler)

    @classmethod
    def enable_logfile(cls, filename='reanimation.log', level=logging.INFO) -> None:
        """
        The journal is output to a file.
        Running it twice can be used to change level.

        :param filename: file to write.
        :param level: logging level
        :return: None
        """
        cls.journal.handlers = [h for h in cls.journal.handlers if h.name != 'logfile']
        handler = logging.FileHandler(filename)
        handler.setLevel(level)
        handler.set_name('logfile')
        handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s - %(message)s'))
        cls.journal.addHandler(handler)
        # logging.getLogger('py.warnings').addHandler(handler)

    @classmethod
    def log_errors(cls):
        """
        RDKit spits a few warning and errors.
        Pyrosetta sends messages to stdout. I might implement a tracer capturing.
        This makes them inline with the logger.

        :return:
        """
        Chem.WrapLogs()
        sys.stderr = LoggerWriter(cls.journal.warning)

    @classmethod
    def slack_me(cls, msg: str) -> bool:
        """
        Send message to a slack webhook

        :param msg: Can be dirty and unicode-y.
        :return: did it work?
        :rtype: bool
        """
        webhook = os.environ['SLACK_WEBHOOK']
        # sanitise.
        msg = unicodedata.normalize('NFKD', msg).encode('ascii', 'ignore').decode('ascii')
        msg = re.sub('[^\w\s\-.,;?!@#()\[\]]', '', msg)
        r = requests.post(url=webhook,
                          headers={'Content-type': 'application/json'},
                          data=f"{{'text': '{msg}'}}")
        if r.status_code == 200 and r.content == b'ok':
            return True
        else:
            return False

    # =================== Other ========================================================================================

    @classmethod
    def copy_names(cls, acceptor_mol: Chem.Mol, donor_mol: Chem.Mol):
        """
        Copy names form donor to acceptor by finding MCS.
        Does it properly and uses ``PDBResidueInfo``.

        :param acceptor_mol: needs atomnames
        :param donor_mol: has atomnames
        :return:
        """
        mcs = rdFMCS.FindMCS([acceptor_mol, donor_mol],
                             atomCompare=rdFMCS.AtomCompare.CompareElements,
                             bondCompare=rdFMCS.BondCompare.CompareOrder,
                             ringMatchesRingOnly=True)
        common = Chem.MolFromSmarts(mcs.smartsString)
        pos_match = acceptor_mol.positioned_mol.GetSubstructMatch(common)
        pdb_match = donor_mol.GetSubstructMatch(common)
        for m, p in zip(pos_match, pdb_match):
            ma = acceptor_mol.GetAtomWithIdx(m)
            pa = donor_mol.GetAtomWithIdx(p)
            assert ma.GetSymbol() == pa.GetSymbol(), 'The indices do not align! ' + \
                                                     f'{ma.GetIdx()}:{ma.GetSymbol()} vs. ' + \
                                                     f'{pa.GetIdx()}:{pa.GetSymbol()}'
            ma.SetMonomerInfo(pa.GetPDBResidueInfo())

    @classmethod
    def add_constraint_to_warhead(cls, name: str, constraint: str):
        """
        Add a constraint (multiline is fine) to a warhead definition.
        This will be added and run by Igor's minimiser.

        :param name:
        :param constraint:
        :return: None
        """
        for war_def in cls.warhead_definitions:
            if war_def['name'] == name:
                war_def['constraint'] = constraint
                break
        else:
            raise ValueError(f'{name} not found in warhead_definitions.')

    @classmethod
    def distance_hits(cls, pdb_filenames: List[str],
                      target_resi: int,
                      target_chain: str,
                      target_atomname: str,
                      ligand_resn='LIG') -> List[float]:
        """
        See closest hit for info.

        :param pdb_filenames:
        :param target_resi:
        :param target_chain:
        :param target_atomname:
        :param ligand_resn:
        :return:
        """
        distances = []
        with pymol2.PyMOL() as pymol:
            for hit in pdb_filenames:
                pymol.cmd.load(hit)
                distances.append(min(
                    [pymol.cmd.distance(f'chain {target_chain} and resi {target_resi} and name {target_atomname}',
                                        f'resn {ligand_resn} and name {atom.name}') for atom in
                     pymol.cmd.get_model(f'resn {ligand_resn}').atom]))
                pymol.cmd.delete('*')
        return distances

    @classmethod
    def closest_hit(cls, pdb_filenames: List[str],
                    target_resi: int,
                    target_chain: str,
                    target_atomname: str,
                    ligand_resn='LIG') -> str:
        """
        This classmethod helps choose which pdb based on which is closer to a given atom.

        :param pdb_filenames:
        :param target_resi:
        :param target_chain:
        :param target_atomname:
        :param ligand_resn:
        :return:
        """
        best_d = 99999
        best_hit = -1
        for hit, d in zip(pdb_filenames,
                          cls.distance_hits(pdb_filenames, target_resi, target_chain, target_atomname, ligand_resn)):
            if d < best_d:
                best_hit = hit
                best_d = d
        return best_hit

    @classmethod
    def make_covalent(cls, smiles: str,
                      warhead_name: Optional[str] = None) -> Union[str, None]:
        """
        Convert a unreacted warhead to a reacted one in the SMILES

        :param smiles: unreacted SMILES
        :param warhead_name: name in the definitions. If unspecified it will try and guess (less preferrable)
        :return: SMILES
        """
        mol = Chem.MolFromSmiles(smiles)
        if warhead_name:
            war_defs = cls._get_warhead_definitions(warhead_name)
        else:
            war_defs = cls.warhead_definitions
        for war_def in war_defs:
            ncv = Chem.MolFromSmiles(war_def['noncovalent'])
            cv = Chem.MolFromSmiles(war_def['covalent'])
            if mol.HasSubstructMatch(ncv):
                x = Chem.ReplaceSubstructs(mol, ncv, cv, replacementConnectionPoint=0)[0]
                return Chem.MolToSmiles(x)
        else:
            return None

    @classmethod
    def get_warhead_definition(cls, warhead_name: str):
        return cls._get_warhead_definitions(warhead_name)[0]

    @classmethod
    def _get_warhead_definitions(cls, warhead_name: str):
        """
        It is unlikely that alternative definitions are present. hence why hidden method.

        :param warhead_name:
        :return:
        """
        options = [wd for wd in cls.warhead_definitions if wd['name'] == warhead_name.lower()]
        if len(options) == 0:
            raise ValueError(f'{warhead_name} is not valid.')
        else:
            return options

    @classmethod
    def make_all_warhead_combinations(cls, smiles: str, warhead_name: str, canonical=True) -> Union[dict, None]:
        """
        Convert a unreacted warhead to a reacted one in the SMILES

        :param smiles: unreacted SMILES
        :param warhead_name: name in the definitions
        :param canonical: the SMILES canonical? (makes sense...)
        :return: dictionary of SMILES
        """
        mol = Chem.MolFromSmiles(smiles)
        war_def = cls.get_warhead_definition(warhead_name)
        ncv = Chem.MolFromSmiles(war_def['noncovalent'])
        if mol.HasSubstructMatch(ncv):
            combinations = {}
            for wd in cls.warhead_definitions:
                x = Chem.ReplaceSubstructs(mol, ncv, Chem.MolFromSmiles(wd['covalent']),
                                           replacementConnectionPoint=0)
                combinations[wd['name'] + '_covalent'] = Chem.MolToSmiles(x[0], canonical=canonical)
                x = Chem.ReplaceSubstructs(mol, ncv, Chem.MolFromSmiles(wd['noncovalent']),
                                           replacementConnectionPoint=0)
                combinations[wd['name'] + '_noncovalent'] = Chem.MolToSmiles(x[0], canonical=canonical)
            return combinations
        else:
            return None

    # =================== pre-encounter ================================================================================

    # @classmethod

    # =================== save  ========================================================================================

    def make_pse(self, filename: str = 'combo.pse', extra_mols:Optional[Chem.Mol]=None):
        """
        Save a pse in the relevant folder. This is the Victor one.

        :param filename:
        :return:
        """
        assert '.pse' in filename, f'{filename} not .pse file'
        with pymol2.PyMOL() as pymol:
            for hit in self.hits:
                hit_name = hit.GetProp('_Name')
                pymol.cmd.read_molstr(Chem.MolToMolBlock(hit), hit_name)
                if self.fragmenstein is None:
                    pymol.cmd.color('grey50', f'element C and {hit_name}')
                elif hit_name in self.fragmenstein.unmatched:
                    pymol.cmd.color('black', f'element C and {hit_name}')
                else:
                    pymol.cmd.color('white', f'element C and {hit_name}')
            if self.fragmenstein is not None and self.fragmenstein.positioned_mol is not None:
                pymol.cmd.read_molstr(Chem.MolToMolBlock(self.fragmenstein.positioned_mol), 'placed')
                pymol.cmd.color('magenta', f'element C and placed')
                pymol.cmd.zoom('byres (placed expand 4)')
                pymol.cmd.show('line', 'byres (placed around 4)')
            if self.minimised_mol is not None:
                pymol.cmd.read_molstr(Chem.MolToMolBlock(self.minimised_mol), 'minimised')
                pymol.cmd.color('green', f'element C and minimised')
            if self.minimised_pdbblock is not None:
                pymol.cmd.read_pdbstr(self.minimised_pdbblock, 'min_protein')
                pymol.cmd.color('gray50', f'element C and min_protein')
                pymol.cmd.hide('sticks', 'min_protein')
            if self.unminimised_pdbblock is not None:
                pymol.cmd.read_pdbstr(self.unminimised_pdbblock, 'unmin_protein')
                pymol.cmd.color('gray20', f'element C and unmin_protein')
                pymol.cmd.hide('sticks', 'unmin_protein')
                pymol.cmd.disable('unmin_protein')
            if extra_mols:
                for mol in extra_mols:
                    name = mol.GetProp('_Name')
                    pymol.cmd.read_molstr(Chem.MolToMolBlock(mol, kekulize=False), name)
                    pymol.cmd.color('magenta', f'{name} and name C*')
            pymol.cmd.save(os.path.join(self.work_path, self.long_name, filename))

    def make_steps_pse(self, filename: str='step.pse'):
        assert '.pse' in filename, f'{filename} not .pse file'
        with pymol2.PyMOL() as pymol:
            for hit in self.hits:
                pymol.cmd.read_molstr(Chem.MolToMolBlock(hit, kekulize=False), hit.GetProp('_Name'))
            for i, mod in enumerate(self.modifications):
                pymol.cmd.read_molstr(Chem.MolToMolBlock(mod, kekulize=False), f'step{i}')
            pymol.cmd.save(os.path.join(self.work_path, self.long_name, filename))

    # =================== extract_mols =================================================================================

    @classmethod
    def find_attachment(cls, pdb: Chem.Mol, ligand_resn: str) -> Tuple[Union[Chem.Atom, None], Union[Chem.Atom, None]]:
        """
        Finds the two atoms in a crosslink bond without looking at LINK record

        :param pdb: a rdkit Chem object
        :param ligand_resn: 3 letter code
        :return: tuple of non-ligand atom and ligand atom
        """
        for atom in pdb.GetAtoms():
            if atom.GetPDBResidueInfo().GetResidueName() == ligand_resn:
                for neigh in atom.GetNeighbors():
                    if neigh.GetPDBResidueInfo().GetResidueName() != ligand_resn:
                        attachment = neigh
                        attachee = atom
                        return (attachment, attachee)
        else:
            attachment = None
            attachee = None
            return (attachment, attachee)

    @classmethod
    def find_closest_to_ligand(cls, pdb: Chem.Mol, ligand_resn: str) -> Tuple[Chem.Atom, Chem.Atom]:
        """
        Find the closest atom to the ligand

        :param pdb: a rdkit Chem object
        :param ligand_resn: 3 letter code
        :return: tuple of non-ligand atom and ligand atom
        """
        ligand = [atom.GetIdx() for atom in pdb.GetAtoms() if atom.GetPDBResidueInfo().GetResidueName() == ligand_resn]
        dm = Chem.Get3DDistanceMatrix(pdb)
        mini = np.take(dm, ligand, 0)
        mini[mini == 0] = np.nan
        mini[:, ligand] = np.nan
        a, b = np.where(mini == np.nanmin(mini))
        lig_atom = pdb.GetAtomWithIdx(ligand[int(a[0])])
        nonlig_atom = pdb.GetAtomWithIdx(int(b[0]))
        return (nonlig_atom, lig_atom)


    @classmethod
    def extract_mols(cls,
                     folder: str,
                     smilesdex: Dict[str, str],
                     ligand_resn: str = 'LIG',
                     regex_name: Optional[str]= None,
                     throw_on_error:bool=False) -> Dict[str, Chem.Mol]:
        """
         A key requirement for Fragmenstein is a separate mol file for the inspiration hits.
        This is however often a pdb. This converts.
        `igor.mol_from_pose()` is similar but works on a pose. `_fix_minimised()` calls mol_from_pose.

        See ``extract_mol`` for single.

        :param folder: folder with pdbs
        :return:
        """
        mols = {}
        for file in os.listdir(folder):
            if '.pdb' not in file:
                continue
            else:
                fullfile = os.path.join(folder, file)
                if regex_name is None:
                    name = os.path.splitext(file)[0]
                elif re.search(regex_name, file) is None:
                    continue
                else:
                    name = re.search(regex_name, file).group(1)
                if name in smilesdex:
                    smiles=smilesdex[name]
                elif throw_on_error:
                    raise ValueError(f'{name} could not be matched to a smiles.')
                else:
                    cls.journal.warning(f'{name} could not be matched to a smiles.')
                    smiles = None
                try:
                    mol = cls.extract_mol(name=name,
                                          filepath=fullfile,
                                          smiles=smiles,
                                          ligand_resn=ligand_resn)
                    if mol is not None:
                        mols[name] = mol
                except Exception as error:
                    if throw_on_error:
                        raise error
                    cls.journal.error(f'{error.__class__.__name__} for {name} - {error}')
        return mols

    @classmethod
    def extract_mol(cls,
                     name: str,
                     filepath: str,
                     smiles: Optional[str] = None,
                     ligand_resn: str = 'LIG',
                     removeHs: bool = False,
                     throw_on_error : bool = False) -> Chem.Mol:
        """
        Extracts the ligand of 3-name ``ligand_resn`` from the PDB file ``filepath``.
        Corrects the bond order with SMILES if given.
        If there is a covalent bond with another residue the bond is kept as a ``*``/R.
        If the SMILES provided lacks the ``*`` element, the SMILES will be converted (if a warhead is matched),
        making the bond order correction okay.

        :param name: name of ligand
        :type name: str
        :param filepath: PDB file
        :type filepath: str
        :param smiles: SMILES
        :type smiles: str
        :param ligand_resn: 3letter PDB name of residue of ligand
        :type ligand_resn: str
        :param removeHs: Do you trust the hydrgens in the the PDB file?
        :type removeHs: bool
        :param throw_on_error: If an error occurs in the template step, raise error.
        :type throw_on_error: bool
        :return: rdkit Chem object
        :rtype: Chem.Mol
        """
        holo = Chem.MolFromPDBFile(filepath, proximityBonding=False, removeHs=removeHs)
        if holo is None:
            cls.journal.warning(f'PDB {filepath} is problematic. Skipping sanitization.')
            holo = Chem.MolFromPDBFile(filepath, proximityBonding=False, removeHs=True, sanitize=False)
        mol = Chem.SplitMolByPDBResidues(holo, whiteList=[ligand_resn])[ligand_resn]
        attachment, attachee = cls.find_attachment(holo, ligand_resn)
        if attachment is not None:  # covalent
            mol = Chem.SplitMolByPDBResidues(holo, whiteList=[ligand_resn])[ligand_resn]
            mod = Chem.RWMol(mol)
            attachment.SetAtomicNum(0)  # dummy atom.
            attachment.GetPDBResidueInfo().SetName('CONN')
            pos = holo.GetConformer().GetAtomPosition(attachment.GetIdx())
            ni = mod.AddAtom(attachment)
            mod.GetConformer().SetAtomPosition(ni, pos)
            attachee_name = attachee.GetPDBResidueInfo().GetName()
            for atom in mod.GetAtoms():
                if atom.GetPDBResidueInfo().GetName() == attachee_name:
                    ai = atom.GetIdx()
                    mod.AddBond(ai, ni, Chem.BondType.SINGLE)
                    break
            mol = mod.GetMol()
        if smiles is not None:
            if '*' in Chem.MolToSmiles(mol) and '*' not in smiles:
                new_smiles = cls.make_covalent(smiles)
                if new_smiles:
                    cls.journal.info(f'{name} is covalent but a non covalent SMILES was passed, which was converted')
                    smiles = new_smiles
                else:
                    cls.journal.warning(f'{name} is covalent but a non covalent SMILES was passed, which failed to convert')
            else:
                pass
            try:
                template = Chem.MolFromSmiles(smiles)
                # template = AllChem.DeleteSubstructs(template, Chem.MolFromSmiles('*'))
                mol = AllChem.AssignBondOrdersFromTemplate(template, mol)
            except ValueError as error:
                if throw_on_error:
                    raise error
                else:
                    cls.journal.warning(f'{name} failed at template-guided bond order correction - ({type(error)}: {error}).')
        mol.SetProp('_Name', name)
        return mol








    # =================== From Files ===================================================================================

    @classmethod
    def from_files(cls, folder: str) -> _VictorBaseMixin:
        """
        This creates an instance form the output files. Likely to be unstable.
        Assumes the checkpoints were not altered.
        And is basically for analysis only.

        :param folder: path
        :return:
        """
        cls.journal.warning('`from_files`: You really should not use this.')
        if os.path.exists(folder):
            pass # folder is fine
        elif not os.path.exists(folder) and os.path.exists(os.path.join(cls.work_path, folder)):
            folder = os.path.join(cls.work_path, folder)
        else:
            raise FileNotFoundError(f'Folder {folder} does not exist.')
        self = cls.__new__(cls)
        self.tick = float('nan')
        self.tock = float('nan')
        self.ligand_resn = ''
        self.ligand_resi = ''
        self.covalent_resn = ''
        self.covalent_resi = ''
        self.hits = []
        self.long_name = os.path.split(folder)[1]
        paramsfiles = os.path.join(folder, f'{self.long_name}.params')
        paramstemp = os.path.join(folder, f'{self.long_name}.params_template.mol')
        if os.path.exists(paramsfiles):
            self.params = Params().load(paramsfiles)
            self.unbound_pose = self.params.test()
            if os.path.exists(paramstemp):
                self.params.mol = Chem.MolFromMolFile(paramstemp, removeHs=False)
        else:
            self.params = None
        posmol = os.path.join(folder, f'{self.long_name}.positioned.mol')
        if os.path.exists(posmol):
            self.mol = Chem.MolFromMolFile(posmol, sanitize=False, removeHs=False)
        else:
            self.journal.info(f'{self.long_name} - no positioned mol')
            self.mol = None
        fragjson = os.path.join(folder, f'{self.long_name}.fragmenstein.json')
        if os.path.exists(fragjson):
            fd = json.load(open(fragjson))
            self.smiles = fd['smiles']
            self.is_covalent = True if '*' in self.smiles else False
            self.fragmenstein = Fragmenstein(mol=self.mol,
                                             hits=self.hits,
                                             attachment=None,
                                             merging_mode='off',
                                             average_position=self.fragmenstein_average_position
                                             )
            self.fragmenstein.positioned_mol = self.mol
            self.fragmenstein.positioned_mol.SetProp('_Origins', json.dumps(fd['origin']))

        else:
            self.is_covalent = None
            self.smiles = ''
            self.fragmenstein = None
            self.journal.info(f'{self.long_name} - no fragmenstein json')
            self.N_constrained_atoms = float('nan')

        #
        self.apo_pdbblock = None
        #
        self.atomnames = None
        #
        self.extra_constraint = ''
        self.pose_fx = None
        # these are calculated
        self.constraint = None
        self.unminimised_pdbblock = None
        self.igor = None
        self.minimised_pdbblock = None
        # buffers etc.
        self._warned = []
        minjson = os.path.join(folder, f'{self.long_name}.minimised.json')
        self.mrmsd = mRSMD.mock()
        if os.path.exists(minjson):
            md = json.load(open(minjson))
            self.energy_score = md["Energy"]
            self.mrmsd.mrmsd = md["mRMSD"]
            self.mrmsd.rmsds = md["RMSDs"]
            self.igor = Igor.from_pdbfile(
                pdbfile=os.path.join(self.work_path, self.long_name, self.long_name + '.holo_minimised.pdb'),
                params_file=os.path.join(self.work_path, self.long_name, self.long_name + '.params'),
                constraint_file=os.path.join(self.work_path, self.long_name, self.long_name + '.con'))
        else:
            self.energy_score = {'ligand_ref2015': {'total_score': float('nan')},
                                 'unbound_ref2015': {'total_score': float('nan')}}

            self.journal.info(f'{self.long_name} - no min json')
        minmol = os.path.join(folder, f'{self.long_name}.minimised.mol')
        if os.path.exists(minmol):
            self.minimised_mol = Chem.MolFromMolFile(minmol, sanitize=False, removeHs=False)
        else:
            self.minimised_mol = None
        return self

    # =================== Laboratory ===================================================================================

    @classmethod
    def laboratory(cls, entries: List[dict], cores: int = 1):
        raise NotImplementedError('Not yet written.')
        pass
Exemple #11
0
 def test_load(self):
     p = Params.load('example/official_PHE.params')
     self.assertEqual(p.NAME, 'PHE')
Exemple #12
0
 def test_renames(self):
     p = Params.from_smiles('CC(ONC)O', atomnames=['CA', 'CB', 'OX', 'ON', 'CX', 'CG'])
     p.rename_atom_by_name('CA', 'CZ')
     self.assertEqual(p.mol.GetAtomWithIdx(0).GetPDBResidueInfo().GetName(), ' CZ ')
     self.assertEqual(p.ATOM[0].name, ' CZ ')
     p.test()
Exemple #13
0
 def test_smiles(self):
     p = Params.from_smiles('*C(=O)C(Cc1ccccc1)[NH]*',  # recognised as amino acid.
                            name='PHX',  # optional.
                            atomnames={3: 'CZ'}  # optional, rando atom name
                            )
     self.assertTrue(p.is_aminoacid())
 def _vanalyse(self):
     # THIS IS A COPY PASTE EXCEPT FOR REANIMATE and Params!!
     #self._assert_inputs()
     # ***** PARAMS & CONSTRAINT *******
     self.journal.info(f'{self.long_name} - Starting work')
     self._log_warnings()
     # making folder.
     self._make_output_folder()
     # make params
     self.journal.debug(f'{self.long_name} - Starting parameterisation')
     self.params = Params.from_smiles_w_pdbfile(pdb_file=self.pdb_filename,
                                                smiles=self.smiles,
                                                generic=False,
                                                name=self.ligand_resn,
                                                proximityBonding=False)
     self.journal.warning(f'{self.long_name} - CHI HAS BEEN DISABLED')
     self.params.CHI.data = []  # TODO fix chi
     self.mol = self.params.mol
     self._log_warnings()
     # get constraint
     self.constraint = self._get_constraint(self.extra_constraint)
     attachment = self._get_attachment_from_pdbblock(
     ) if self.is_covalent else None
     self._log_warnings()
     self.post_params_step()
     # ***** FRAGMENSTEIN *******
     # make fragmenstein
     attachment = self._get_attachment_from_pdbblock(
     ) if self.is_covalent else None
     self.journal.debug(f'{self.long_name} - Starting fragmenstein')
     self.fragmenstein = Fragmenstein(
         mol=self.params.mol,  #Chem.MolFromSmiles(self.smiles)
         hits=self.hits,
         attachment=attachment,
         merging_mode=self.fragmenstein_merging_mode,
         debug_draw=self.fragmenstein_debug_draw,
         average_position=self.fragmenstein_average_position)
     self.constraint.custom_constraint += self._make_coordinate_constraints(
     )
     self._checkpoint_bravo()
     # save stuff
     params_file, holo_file, constraint_file = self._save_prerequisites()
     self.post_fragmenstein_step()
     self.unbound_pose = self.params.test()
     self._checkpoint_alpha()
     # ***** EGOR *******
     self.journal.debug(f'{self.long_name} - setting up Igor')
     self.igor = Igor.from_pdbblock(pdbblock=self.unminimised_pdbblock,
                                    params_file=params_file,
                                    constraint_file=constraint_file,
                                    ligand_residue=self.ligand_resi,
                                    key_residues=[self.covalent_resi])
     # user custom code.
     if self.pose_fx is not None:
         self.journal.debug(f'{self.long_name} - running custom pose mod.')
         self.pose_fx(self.igor.pose)
     else:
         self.pose_mod_step()
     # storing a roundtrip
     self.unminimised_pdbblock = self.igor.pose2str()
     # DO NOT DO ddG = self.reanimate()
     ddG = self.prod()
     self.minimised_pdbblock = self.igor.pose2str()
     self.post_igor_step()
     self.minimised_mol = self._fix_minimised()
     self.mrmsd = self._calculate_rmsd()
     self.journal.info(
         f'{self.long_name} - final score: {ddG} kcal/mol {self.mrmsd.mrmsd}.'
     )
     self._checkpoint_charlie()
     self.journal.debug(f'{self.long_name} - Completed')