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)
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)
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')
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))
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)
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')
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
def test_load(self): p = Params.load('example/official_PHE.params') self.assertEqual(p.NAME, 'PHE')
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()
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')