def OERMSD_Array(refcrds, fitcrds, size): oechem.OERMSD(refcrds, fitcrds, size) overlay = True oechem.OERMSD(refcrds, fitcrds, size, overlay) rotmat = oechem.OEDoubleArray(9) oechem.OERMSD(refcrds, fitcrds, size, overlay, rotmat) transvec = oechem.OEDoubleArray(3) oechem.OERMSD(refcrds, fitcrds, size, overlay, rotmat, transvec)
def OERMSD_Part_MolBase(ref, fit): match = oechem.OEMatch() for aRef, aFit in zip(ref.GetAtoms(), fit.GetAtoms()): match.AddPair(aRef, aFit) oechem.OERMSD(ref, fit, match) overlay = True oechem.OERMSD(ref, fit, match, overlay) rotmat = oechem.OEDoubleArray(9) oechem.OERMSD(ref, fit, match, overlay, rotmat) transvec = oechem.OEDoubleArray(3) oechem.OERMSD(ref, fit, match, overlay, rotmat, transvec)
def _compute_rmsd_matrix_oe(self, molecule: Molecule) -> numpy.ndarray: """Computes the RMSD between all conformers stored on a molecule using an OpenEye backend.""" from openeye import oechem oe_molecule: oechem.OEMol = molecule.to_openeye() oe_conformers = { i: oe_conformer for i, oe_conformer in enumerate(oe_molecule.GetConfs()) } n_conformers = len(molecule.conformers) rmsd_matrix = numpy.zeros((n_conformers, n_conformers)) for i, j in itertools.combinations([*oe_conformers], 2): rmsd_matrix[i, j] = oechem.OERMSD( oe_conformers[i], oe_conformers[j], self.check_automorphs, self.heavy_atoms_only, True, ) rmsd_matrix += rmsd_matrix.T return rmsd_matrix
def OERMSD_Part_MCMolBase(ref, fit): match = oechem.OEMatch() for aRef, aFit in zip(ref.GetAtoms(), fit.GetAtoms()): match.AddPair(aRef, aFit) nConfs = fit.GetMaxConfIdx() vecRmsd = oechem.OEDoubleArray(nConfs) oechem.OERMSD(ref, fit, vecRmsd, match) overlay = True oechem.OERMSD(ref, fit, vecRmsd, match, overlay) rotmat = oechem.OEDoubleArray(9*nConfs) oechem.OERMSD(ref, fit, vecRmsd, match, overlay, rotmat) transvec = oechem.OEDoubleArray(3*nConfs) oechem.OERMSD(ref, fit, vecRmsd, match, overlay, rotmat, transvec)
def main(argv=[__name__]): itf = oechem.OEInterface(InterfaceData, argv) if not itf.GetBool("-verbose"): oechem.OEThrow.SetLevel(oechem.OEErrorLevel_Warning) rfname = itf.GetString("-ref") ifname = itf.GetString("-in") automorph = itf.GetBool("-automorph") heavy = itf.GetBool("-heavyonly") overlay = itf.GetBool("-overlay") ifs = oechem.oemolistream() if not ifs.open(rfname): oechem.OEThrow.Fatal("Unable to open %s for reading" % rfname) rmol = oechem.OEGraphMol() if not oechem.OEReadMolecule(ifs, rmol): oechem.OEThrow.Fatal("Unable to read reference molecule") ifs = oechem.oemolistream() if not ifs.open(ifname): oechem.OEThrow.Fatal("Unable to open %s for reading" % ifname) ofs = oechem.oemolostream() if itf.HasString("-out"): ofname = itf.GetString("-out") if not ofs.open(ofname): oechem.OEThrow.Fatal("Unable to open %s for writing" % ofname) if not overlay: oechem.OEThrow.Warning( "Output is the same as input when overlay is false") for mol in ifs.GetOEMols(): oechem.OEThrow.Info(mol.GetTitle()) rmsds = oechem.OEDoubleArray(mol.GetMaxConfIdx()) rmtx = oechem.OEDoubleArray(9 * mol.GetMaxConfIdx()) tmtx = oechem.OEDoubleArray(3 * mol.GetMaxConfIdx()) # perform RMSD for all confomers oechem.OERMSD(rmol, mol, rmsds, automorph, heavy, overlay, rmtx, tmtx) for conf in mol.GetConfs(): cidx = conf.GetIdx() oechem.OEThrow.Info("Conformer %i : rmsd = %f" % (cidx, rmsds[cidx])) if itf.GetBool("-overlay"): oechem.OERotate(conf, rmtx[cidx * 9:cidx * 9 + 9]) oechem.OETranslate(conf, tmtx[cidx * 3:cidx * 3 + 3]) if itf.HasString("-out"): oechem.OEWriteMolecule(ofs, mol) return 0
def compare_two_mols(rmol, qmol, rmsd_cutoff): """ For two identical molecules, with varying conformers, make an M by N comparison to match the M minima of rmol to the N minima of qmol. Match is declared for lowest RMSD between the two conformers and if the RMSD is below rmsd_cutoff. Parameters ---------- rmol : OEMol reference OEChem molecule with all its filtered conformers qmol : OEMol query OEChem molecule with all its filtered conformers rmsd_cutoff : float cutoff above which two structures are considered diff conformers Returns ------- molIndices : list 1D list of qmol conformer indices that correspond to rmol confs """ automorph = True # take into acct symmetry related transformations heavyOnly = False # do consider hydrogen atoms for automorphisms overlay = True # find the lowest possible RMSD molIndices = [] # 1D list, stores indices of matched qmol confs wrt rmol for ref_conf in rmol.GetConfs(): print(f">>>> Matching {qmol.GetTitle()} conformers to minima: " f"{ref_conf.GetIdx()+1} <<<<") # for this ref_conf, calculate/store RMSDs with all qmol's conformers thisR_allQ = [] for que_conf in qmol.GetConfs(): rms = oechem.OERMSD(ref_conf, que_conf, automorph, heavyOnly, overlay) thisR_allQ.append(rms) # for this ref_conf, get qmol conformer index of min RMSD if <=cutoff lowest_rmsd_index = [ i for i, j in enumerate(thisR_allQ) if j == min(thisR_allQ) ][0] if thisR_allQ[lowest_rmsd_index] <= rmsd_cutoff: molIndices.append(lowest_rmsd_index) else: print('no match bc rmsd is ', thisR_allQ[lowest_rmsd_index]) molIndices.append(None) return molIndices
def CliqueAlign(refmol, fitmol, ofs): cs = oechem.OECliqueSearch(refmol, oechem.OEExprOpts_DefaultAtoms, oechem.OEExprOpts_DefaultBonds) cs.SetSaveRange(5) cs.SetMinAtoms(6) for mi in cs.Match(fitmol): rmat = oechem.OEDoubleArray(9) trans = oechem.OEDoubleArray(3) overlay = True oechem.OERMSD(cs.GetPattern(), fitmol, mi, overlay, rmat, trans) oechem.OERotate(fitmol, rmat) oechem.OETranslate(fitmol, trans) oechem.OEWriteMolecule(ofs, fitmol)
def SmartsAlign(refmol, fitmol, ss, ofs): unique = True for match1 in ss.Match(refmol, unique): for match2 in ss.Match(fitmol, unique): match = oechem.OEMatch() for mp1, mp2 in zip(match1.GetAtoms(), match2.GetAtoms()): match.AddPair(mp1.target, mp2.target) overlay = True rmat = oechem.OEDoubleArray(9) trans = oechem.OEDoubleArray(3) oechem.OERMSD(refmol, fitmol, match, overlay, rmat, trans) oechem.OERotate(fitmol, rmat) oechem.OETranslate(fitmol, trans) oechem.OEWriteConstMolecule(ofs, fitmol)
def RMSD(ref_mol2, query_mol2): """ From one input reference molecule and one input query molecule, the RMSD is computed and returned. Parameters --------- ref_mol2: str - mol2 file of the reference force field query_mol2: str - mol2 file of the query force field Returns ------- rms: float - RMSD in Angstroms """ # open reference molecule ifsRef = oechem.oemolistream(ref_mol2) print("Opening reference molecule:", ref_mol2) # check if the file exist if not ifsRef.open(ref_mol2): print("Unable to locate %s. Skipping." % ref_mol2.split('/')[-1]) # open query molecule #queryFile = ("/work/cluster/nthi/ForceField-Comparison/%s/%s/%s" % (homeDir, ffList, fName) ) print("Opening query molecule: ", query_mol2) if os.path.exists(query_mol2): ifsQuery = oechem.oemolistream(query_mol2) # set flavor for the input file flavor = oechem.OEIFlavor_Generic_Default | oechem.OEIFlavor_MOL2_Default | oechem.OEIFlavor_MOL2_Forcefield ifsRef.SetFlavor(oechem.OEFormat_MOL2, flavor) ifsQuery.SetFlavor(oechem.OEFormat_MOL2, flavor) # create "blank" object rmol = oechem.OEGraphMol() qmol = oechem.OEGraphMol() # load molecule from files oechem.OEReadMolecule(ifsRef, rmol) oechem.OEReadMolecule(ifsQuery, qmol) # calculate rmsd setting automorph, heavyOnly, and overlay to True rms = oechem.OERMSD(rmol, qmol, True, True, True) else: rms = -2 return rms
def compare2Mols(rmol, qmol): """ For two identical molecules, with varying conformers, make an M by N comparison to match the M minima of rmol to the N minima of qmol. Match is declared for lowest RMSD between the two conformers and if the RMSD is below 0.5 Angstrom. Parameters ---------- rmol: reference OEChem molecule with all its filtered conformers qmol: query OEChem molecule with all its filtered conformers Returns ------- molIndices: 1D list of qmol conformer indices that correspond to rmol confs """ automorph = True # take into acct symmetry related transformations heavyOnly = False # do consider hydrogen atoms for automorphisms overlay = True # find the lowest possible RMSD molIndices = [ ] # 1D list for storing indices of matched qmol confs wrt rmol for Rconf in rmol.GetConfs(): print(">>>> Matching %s conformers to minima: %d <<<<"\ % (qmol.GetTitle(),Rconf.GetIdx()+1)) # for this Rconf, calculate/store RMSDs with all of qmol's conformers rsublist = [] for Qconf in qmol.GetConfs(): rms = oechem.OERMSD(Rconf, Qconf, automorph, heavyOnly, overlay) rsublist.append(rms) # for this Rconf, get qmol conformer index for minimum RMSD thisMin = [i for i, j in enumerate(rsublist) if j == min(rsublist)][0] if rsublist[thisMin] <= 0.5: molIndices.append(thisMin) else: print('no match bc rmsd is ', rsublist[thisMin]) molIndices.append(None) return molIndices
def MCSAlign(refmol, fitmol, ofs): atomexpr = oechem.OEExprOpts_AtomicNumber | oechem.OEExprOpts_Aromaticity bondexpr = 0 mcss = oechem.OEMCSSearch(oechem.OEMCSType_Exhaustive) mcss.Init(refmol, atomexpr, bondexpr) mcss.SetMCSFunc(oechem.OEMCSMaxBondsCompleteCycles()) rmat = oechem.OEDoubleArray(9) trans = oechem.OEDoubleArray(3) unique = True overlay = True for match in mcss.Match(fitmol, unique): rms = oechem.OERMSD(mcss.GetPattern(), fitmol, match, overlay, rmat, trans) if rms < 0.0: oechem.OEThrow.Warning("RMS overlay failure") continue oechem.OERotate(fitmol, rmat) oechem.OETranslate(fitmol, trans) oechem.OEWriteMolecule(ofs, fitmol)
def OERMSD_Full_MolBase(ref, fit): oechem.OERMSD(ref, fit) automorf = True oechem.OERMSD(ref, fit, automorf) heavyOnly = True oechem.OERMSD(ref, fit, automorf, heavyOnly) overlay = False oechem.OERMSD(ref, fit, automorf, heavyOnly, overlay) rotmat = oechem.OEDoubleArray(9) oechem.OERMSD(ref, fit, automorf, heavyOnly, overlay, rotmat) transvec = oechem.OEDoubleArray(3) oechem.OERMSD(ref, fit, automorf, heavyOnly, overlay, rotmat, transvec)
def SeqAlign(ref, fit, ofs): sa = oechem.OEGetAlignment(ref, fit) print() print("Alignment of %s to %s" % (fit.GetTitle(), ref.GetTitle())) print() print(" Method: %s" % oechem.OEGetAlignmentMethodName(sa.GetMethod())) print(" Gap : %d" % sa.GetGap()) print(" Extend: %d" % sa.GetExtend()) print(" Score : %d" % sa.GetScore()) print() oss = oechem.oeosstream() oechem.OEWriteAlignment(oss, sa) print(oss.str().decode("UTF-8")) onlyCAlpha = True overlay = True rot = oechem.OEDoubleArray(9) trans = oechem.OEDoubleArray(3) rmsd = oechem.OERMSD(ref, fit, sa, onlyCAlpha, overlay, rot, trans) print(" RMSD = %.1f" % rmsd) oechem.OERotate(fit, rot) oechem.OETranslate(fit, trans) oechem.OEWriteMolecule(ofs, fit)
def OERMSD_Full_MCMolBase(ref, fit): nConfs = fit.GetMaxConfIdx() vecRmsd = oechem.OEDoubleArray(nConfs) oechem.OERMSD(ref, fit, vecRmsd) automorf = True oechem.OERMSD(ref, fit, vecRmsd, automorf) heavyOnly = True oechem.OERMSD(ref, fit, vecRmsd, automorf, heavyOnly) overlay = True oechem.OERMSD(ref, fit, vecRmsd, automorf, heavyOnly, overlay) rotmat = oechem.OEDoubleArray(9*nConfs) oechem.OERMSD(ref, fit, vecRmsd, automorf, heavyOnly, overlay, rotmat) transvec = oechem.OEDoubleArray(3*nConfs) oechem.OERMSD(ref, fit, vecRmsd, automorf, heavyOnly, overlay, rotmat, transvec)
def IdentifyMinima(mol, tag, ThresholdE, ThresholdRMSD): """ For a molecule's set of conformers computed with some level of theory, whittle down unique conformers based on energy and RMSD. Parameters ---------- mol OEChem molecule with all of its conformers tag string name of the SD tag in this molecule ThresholdE float value for abs(E1-E2), below which 2 confs are "same" Units are hartrees (default output units of Psi4) ThresholdR float value for RMSD, below which 2 confs are "same" Units are in Angstrom (Psi4 default) Returns ------- boolean True if successful filter + delete. False if there's only one conf and it didn't optimize, or something else funky. """ # Parameters for OpenEye RMSD calculation automorph = True heavyOnly = False overlay = True # declare variables for conformers to delete confsToDel = set() delCount = 0 # check if SD tag exists for the case of single conformer if mol.NumConfs() == 1: testmol = mol.GetConfs().next() for x in oechem.OEGetSDDataPairs(mol): if tag.lower() in x.GetTag().lower(): return True else: return False # Loop over conformers twice (NxN diagonal comparison of RMSDs) for confRef in mol.GetConfs(): print(" ~ Reference: %s conformer %d" % (mol.GetTitle(), confRef.GetIdx() + 1)) # get real tag (correct for capitalization) for x in oechem.OEGetSDDataPairs(confRef): if tag.lower() in x.GetTag().lower(): taglabel = x.GetTag() # delete cases that don't have energy (opt not converged; or other) if not oechem.OEHasSDData(confRef, taglabel): confsToDel.add(confRef.GetIdx()) delCount += 1 continue refE = float(oechem.OEGetSDData(confRef, taglabel)) for confTest in mol.GetConfs(): # upper right triangle comparison if confTest.GetIdx() <= confRef.GetIdx(): continue # skip cases already set for removal if confTest.GetIdx() in confsToDel: continue # delete cases that don't have energy if not oechem.OEHasSDData(confTest, taglabel): confsToDel.add(confTest.GetIdx()) continue testE = float(oechem.OEGetSDData(confTest, taglabel)) # if MM (not Psi4) energies, convert absERel to Hartrees if 'mm' in taglabel.lower(): absERel = abs(refE - testE) / 627.5095 else: absERel = abs(refE - testE) # if energies are diff enough --> confs are diff --> keep & skip ahead if absERel > ThresholdE: continue # if energies are similar, see if they are diff by RMSD rmsd = oechem.OERMSD(confRef, confTest, automorph, heavyOnly, overlay) # if measured_RMSD < threshold_RMSD --> confs are same --> delete if rmsd < ThresholdRMSD: confsToDel.add(confTest.GetIdx()) # for the same molecule, delete tagged conformers print("%s original number of conformers: %d" % (mol.GetTitle(), mol.NumConfs())) if delCount == mol.NumConfs(): # all conformers in this mol has been tagged for deletion return False for conf in mol.GetConfs(): if conf.GetIdx() in confsToDel: print('Removing %s conformer index %d' % (mol.GetTitle(), conf.GetIdx())) if not mol.DeleteConf(conf): oechem.OEThrow.Fatal("Unable to delete %s GetIdx() %d" \ % (mol.GetTitle(), conf.GetIdx())) return True
def create_merged_topology(system, topology, positions, molecules, softcore_alpha=0.5, softcore_beta=12 * unit.angstrom**2): """ Create an OpenMM system that utilizes a merged topology that can interpolate between many small molecules sharing a common core. Notes ----- * Currently, only one set of molecules is supported. * Residue mutations are not yet supported. Parameters ---------- system : simtk.openmm.System The system representing the environment, not yet containing any molecules. topology : simtk.openmm.app.Topology The topology object corresponding to system. positions : simtk.unit.Quantity of numpy array natoms x 3 compatible with units angstroms The positions array corresponding to system and topology. molecules : list of openeye.oechem.OEMol Molecules to be added to the system. These molecules must share a common core with identical GAFF atom types. softcore_alpha : float, optional, default=0.5 Softcore parameter for Lennard-Jones softening. softcore_beta : simtk.unit.Quantity with units compatible with angstrom**2 Softcore parameter for Coulomb interaction softening. Returns ------- system : simtk.openmm.System Modified version of system in which old system is recovered for global context paramaeter `lambda` = 0 and new molecule is substituted for `lambda` = 1. topology : system.openmm.Topology Topology corresponding to system. """ # # First, process first molecule so we can add common substructure atoms to System along with valence terms among core atoms. # # Use the first molecule as a reference molecule. # TODO: Replace this with a different reference molecule. molecule = molecules[0].CreateCopy() gaff_molecule = gaff_molecules[0].CreateCopy() # Determine common atoms in second molecule. matches = [match for match in mcss.Match(gaff_molecule, True)] match = matches[0] # we only need the first match # Make a list of the atoms in core. reverse_mapping = dict() print "molecule => core mapping" for matchpair in match.GetAtoms(): core_index = matchpair.pattern.GetIdx() molecule_index = matchpair.target.GetIdx() print "%8d => %8d" % (molecule_index, core_index) reverse_mapping[core_index] = molecule_index # Make sure the list of atoms in molecule corresponds to the ordering of atoms within the core. core_atoms = [ reverse_mapping[core_index] for core_index in range(core.NumAtoms()) ] # Create OpenMM Topology and System objects for given molecules using GAFF/AM1-BCC. print "Generating OpenMM system for molecule..." [molecule_system, molecule_topology, molecule_positions] = generate_openmm_system(molecule) # TODO: Replace charges in molecule with RMS charges. # Add core fragment to system. core_mapping = add_molecule_to_system(system, molecule_system, core_atoms, variant=0) # Add molecule to topology as a new residue. chain = topology.addChain() residue = topology.addResidue('COR', chain) atoms = [atom for atom in molecule.GetAtoms() ] # build a list of all atoms in reference molecule atoms = [atoms[index] for index in core_atoms ] # select out only core atoms in proper order for atom in atoms: name = atom.GetName() atomic_number = atom.GetAtomicNum() element = app.Element.getByAtomicNumber(atomic_number) topology.addAtom(name, element, residue) # Append positions of new particles. positions = unit.Quantity( np.append(positions / positions.unit, molecule_positions[core_atoms, :] / positions.unit, axis=0), positions.unit) # Create a running list of atoms all variants should have interactions excluded with. atoms_to_exclude = core_mapping.values() # # Create restraint force to keep corresponding core atoms from molecules near their corresponding core atoms. # TODO: Can we replace these with length-zero constraints or virtual particles if OpenMM supports this in the future? # K = 10.0 * unit.kilocalories_per_mole / unit.angstrom**2 # spring constant energy_expression = '(K/2) * r^2;' energy_expression += 'K = %f;' % K.value_in_unit_system( unit.md_unit_system) restraint_force = mm.CustomBondForce(energy_expression) # # Now, process all molecules. # variants = list() variant = 1 for (molecule, gaff_molecule) in zip(molecules, gaff_molecules): print "" print "*******************************************************" print "Incorporating molecule %s" % molecule.GetTitle() print "*******************************************************" print "" # Determine common atoms in second molecule. matches = [match for match in mcss.Match(gaff_molecule, True)] print matches match = matches[0] # we only need the first match # Make a list of the atoms in core. core_atoms = list() core_atoms_bond_mapping = dict() print "molecule => core mapping" for matchpair in match.GetAtoms(): core_index = matchpair.pattern.GetIdx() molecule_index = matchpair.target.GetIdx() core_atoms.append( molecule_index ) # list of atoms in the molecule that correspond to core atoms print "%8d => %8d" % (molecule_index, core_index) core_atoms_bond_mapping[molecule_index] = core_mapping[ core_index] # index of corresponding core atom in system, for restraining # Align molecule to overlay common core. overlay = True rmat = oe.OEDoubleArray(9) trans = oe.OEDoubleArray(3) rms = oe.OERMSD(mcss.GetPattern(), gaff_molecule, match, overlay, rmat, trans) if rms < 0.0: raise Exception("RMS overlay failure") print "RMSD after overlay is %.3f A" % rms oe.OERotate(gaff_molecule, rmat) oe.OETranslate(gaff_molecule, trans) # Transfer positions to regular molecule with Tripos atom types. gaff_atoms = [atom for atom in gaff_molecule.GetAtoms()] atoms = [atom for atom in molecule.GetAtoms()] for (source_atom, dest_atom) in zip(gaff_atoms, atoms): molecule.SetCoords(atom, gaff_molecule.GetCoords(atom)) # Create OpenMM Topology and System objects for given molecules using GAFF/AM1-BCC. print "Generating OpenMM system for molecule..." [molecule_system, molecule_topology, molecule_positions] = generate_openmm_system(molecule) # Append positions of new particles. positions = unit.Quantity( np.append(positions / positions.unit, molecule_positions / positions.unit, axis=0), positions.unit) # Add valence terms only. mapping = add_molecule_to_system(system, molecule_system, core_atoms, variant=variant, atoms_to_exclude=atoms_to_exclude) # DEBUG print "Atom mappings into System object:" print mapping # Add restraints to keep core atoms from this molecule near their corresponding core atoms. for index in core_atoms: restraint_force.addBond(mapping[index], core_atoms_bond_mapping[index], []) # Add molecule to topology as a new residue. # TODO: Can we simplify this by copying from molecule_topology instead? residue = topology.addResidue('LIG', chain) for atom in atoms: name = atom.GetName() atomic_number = atom.GetAtomicNum() element = app.Element.getByAtomicNumber(atomic_number) topology.addAtom(name, element, residue) # Increment variant index. variant += 1 # Append to list of atoms to be excluded. atoms_to_exclude += mapping.values() print "Done!" # Add restraint force to core atoms. # NOTE: This cannot be added to system earlier because of dict lookup for 'CustomBondForce' in add_valence_terms(). system.addForce(restraint_force) return [system, topology, positions]
def compare_ffs(in_dict, conf_id_tag, out_prefix, mol_slice=None): """ For 2+ SDF files that are analogous in terms of molecules and their conformers, assess them by RMSD, TFD, and relative energy differences. Parameters ---------- in_dict : OrderedDict dictionary from input file, where key is method and value is dictionary first entry should be reference method in sub-dictionary, keys are 'sdfile' and 'sdtag' conf_id_tag : string label of the SD tag that should be the same for matching conformers in different files out_prefix : string prefix appended to sdf file name to write out new SDF file with RMSD and TFD info added as SD tags mol_slice : numpy slice object The resulting integers are numerically sorted and duplicates removed. e.g., slices = np.s_[0, 3:5, 6::3] would be parsed to return [0, 3, 4, 6, 9, 12, 15, 18, ...] Can also parse from end: [-3:] gets the last 3 molecules, and [-2:-1] is the same as [-2] to get just next to last molecule. Returns ------- enes_full : 3D list enes_full[i][j][k] = ddE of ith method, jth mol, kth conformer. ddE = (dE of query method) - (dE of ref method), where the dE is computed as conformer M - conformer N, and conformer N is chosen from the lowest energy of the ref confs. the reference method is not present; i.e., self-comparison is skipped, so the max i value represents total number of files minus one. rmsds_full : 3D list same format as that of enes_full but with conformer RMSDs tfds_full : 3D list same format as that of enes_full but with conformer TFDs smiles_full : 3D list same format as that of enes_full but with conformer SMILES strings """ # set RMSD calculation parameters automorph = True # take into acct symmetry related transformations heavyOnly = False # do consider hydrogen atoms for automorphisms overlay = True # find the lowest possible RMSD # initiate final data lists enes_full = [] rmsds_full = [] tfds_full = [] smiles_full = [] # get first filename representing the reference geometries sdf_ref = list(in_dict.values())[0]['sdfile'] tag_ref = list(in_dict.values())[0]['sdtag'] # assess each file against reference for ff_label, ff_dict in in_dict.items(): # get details of queried file sdf_que = ff_dict['sdfile'] tag_que = ff_dict['sdtag'] if sdf_que == sdf_ref: continue # initiate new sublists enes_method = [] rmsds_method = [] tfds_method = [] smiles_method = [] # open an output file to store query molecules with new SD tags out_file = f'{out_prefix}_{os.path.basename(sdf_que)}' ofs = oechem.oemolostream() if not ofs.open(out_file): oechem.OEThrow.Fatal(f"Unable to open {out_file} for writing") # load molecules from open reference and query files print(f"\n\nOpening reference file {sdf_ref}") mols_ref = reader.read_mols(sdf_ref, mol_slice) print(f"Opening query file {sdf_que} for [ {ff_label} ] energies") mols_que = reader.read_mols(sdf_que, mol_slice) # loop over each molecule in reference and query files for rmol, qmol in zip(mols_ref, mols_que): # initial check that they have same title and number of confs rmol_name = rmol.GetTitle() rmol_nconfs = rmol.NumConfs() if (rmol_name != qmol.GetTitle()) or (rmol_nconfs != qmol.NumConfs()): raise ValueError( "ERROR: Molecules not aligned in iteration. " "Offending molecules and number of conformers:\n" f"\'{rmol_name}\': {rmol_nconfs} nconfs\n" f"\'{qmol.GetTitle()}\': {qmol.NumConfs()} nconfs") # initialize lists to store conformer energies enes_ref = [] enes_que = [] rmsds_mol = [] tfds_mol = [] smiles_mol = [] # loop over each conformer of this mol for ref_conf, que_conf in zip(rmol.GetConfs(), qmol.GetConfs()): # check confomer match from the specified tag ref_id = oechem.OEGetSDData(ref_conf, conf_id_tag) que_id = oechem.OEGetSDData(que_conf, conf_id_tag) if ref_id != que_id: raise ValueError( "ERROR: Conformers not aligned in iteration" f" for mol: '{rmol_name}'. The conformer " f"IDs ({conf_id_tag}) for ref and query are:" f"\n{ref_id}\n{que_id}.") # note the smiles id smiles_mol.append(ref_id) # get energies enes_ref.append(float(oechem.OEGetSDData(ref_conf, tag_ref))) enes_que.append(float(oechem.OEGetSDData(que_conf, tag_que))) # compute RMSD between reference and query conformers rmsd = oechem.OERMSD(ref_conf, que_conf, automorph, heavyOnly, overlay) rmsds_mol.append(rmsd) # compute TFD between reference and query conformers tfd = calc_tfd(ref_conf, que_conf, conf_id_tag) tfds_mol.append(tfd) # store data in SD tags for query conf, and write conf to file oechem.OEAddSDData(que_conf, f'RMSD to {sdf_ref}', str(rmsd)) oechem.OEAddSDData(que_conf, f'TFD to {sdf_ref}', str(tfd)) oechem.OEWriteConstMolecule(ofs, que_conf) # compute relative energies against lowest E reference conformer lowest_ref_idx = enes_ref.index(min(enes_ref)) rel_enes_ref = np.array(enes_ref) - enes_ref[lowest_ref_idx] rel_enes_que = np.array(enes_que) - enes_que[lowest_ref_idx] # subtract them to get ddE = dE (query method) - dE (ref method) enes_mol = np.array(rel_enes_que) - np.array(rel_enes_ref) # store then move on enes_method.append(enes_mol) rmsds_method.append(np.array(rmsds_mol)) tfds_method.append(np.array(tfds_mol)) smiles_method.append(smiles_mol) #print(rmsds_method, len(rmsds_method)) #print(enes_method, len(enes_method)) enes_full.append(enes_method) rmsds_full.append(rmsds_method) tfds_full.append(tfds_method) smiles_full.append(smiles_method) ofs.close() return enes_full, rmsds_full, tfds_full, smiles_full
def create_relative_alchemical_transformation(system, topology, positions, molecule1_indices_in_system, molecule1, molecule2, softcore_alpha=0.5, softcore_beta=12*unit.angstrom**2): """ Create an OpenMM System object to handle the alchemical transformation from molecule1 to molecule2. system : simtk.openmm.System The system to be modified, already containing molecule1 whose atoms correspond to molecule1_indices_in_system. topology : simtk.openmm.app.Topology The topology object corresponding to system. positions : simtk.unit.Quantity of numpy array natoms x 3 compatible with units angstroms The positions array corresponding to system and topology. molecule1_indices_in_system : list of int Indices of molecule1 in system, with atoms in same order. molecule1 : openeye.oechem.OEMol Molecule already present in system, where the atom mapping is given by molecule1_indices_in_system. molecule2 : openeye.oechem.OEMol New molecule that molecule1 will be transformed into as lambda parameter goes from 0 -> 1. softcore_alpha : float, optional, default=0.5 Softcore parameter for Lennard-Jones softening. softcore_beta : simtk.unit.Quantity with units compatible with angstrom**2 Softcore parameter for Coulomb interaction softening. Returns ------- system : simtk.openmm.System Modified version of system in which old system is recovered for global context paramaeter `lambda` = 0 and new molecule is substituted for `lambda` = 1. topology : system.openmm.Topology Topology corresponding to system. """ # Copy molecules. molecule1 = oe.OEMol(molecule1) molecule2 = oe.OEMol(molecule2) # Normalize molecules. # TODO: May need to do more normalization here. oe.OEPerceiveChiral(molecule1) oe.OEPerceiveChiral(molecule2) # Make copies to not destroy original objects. import copy system = copy.deepcopy(system) topology = copy.deepcopy(topology) positions = copy.deepcopy(positions) # Create lists of corresponding atoms for common substructure and groups specific to molecules 1 and 2. atomexpr = oe.OEExprOpts_DefaultAtoms bondexpr = oe.OEExprOpts_BondOrder | oe.OEExprOpts_EqSingleDouble | oe.OEExprOpts_EqAromatic mcss = oe.OEMCSSearch(molecule1, atomexpr, bondexpr, oe.OEMCSType_Exhaustive) # This modifies scoring function to prefer keeping cycles complete. mcss.SetMCSFunc( oe.OEMCSMaxAtomsCompleteCycles() ) # TODO: Set initial substructure size? # mcss.SetMinAtoms( some_number ) # We only need one match. mcss.SetMaxMatches(1) # Determine common atoms in second molecule. matches = [ match for match in mcss.Match(molecule2, True) ] match = matches[0] # we only need the first match # Align common substructure of molecule2 with molecule1. overlay = True rmat = oe.OEDoubleArray(9) trans = oe.OEDoubleArray(3) rms = oe.OERMSD(mcss.GetPattern(), molecule2, match, overlay, rmat, trans) if rms < 0.0: raise Exception("RMS overlay failure") oe.OERotate(molecule2, rmat) oe.OETranslate(molecule2, trans) # Make a list of the atoms in common, molecule1 only, and molecule2 only common1 = list() # list of atom indices in molecule1 that also appear in molecule2 common2 = list() # list of atom indices in molecule2 that also appear in molecule1 unique1 = list() # list of atom indices in molecule1 that DO NOT appear in molecule2 unique2 = list() # list of atom indices in molecule2 that DO NOT appear in molecule1 mapping1 = dict() # mapping of atoms in molecule1 to molecule2 mapping2 = dict() # mapping of atoms in molecule2 to molecule1 for matchpair in match.GetAtoms(): index1 = matchpair.pattern.GetIdx() index2 = matchpair.target.GetIdx() mapping1[ index1 ] = index2 mapping2[ index2 ] = index1 all1 = frozenset(range(molecule1.NumAtoms())) all2 = frozenset(range(molecule2.NumAtoms())) common1 = frozenset(mapping1.keys()) common2 = frozenset(mapping2.keys()) unique1 = all1 - common1 unique2 = all2 - common2 # DEBUG print "list of atoms common to both molecules:" print "molecule1: %s" % str(common1) print "molecule2: %s" % str(common2) print "list of atoms unqiue to individual molecules:" print "molecule1: %s" % str(unique1) print "molecule2: %s" % str(unique2) print "MAPPING FROM MOLECULE1 TO MOLECULE2" for atom1 in mapping1.keys(): atom2 = mapping1[atom1] print "%5d => %5d" % (atom1, atom2) # Create OpenMM Topology and System objects for given molecules using GAFF/AM1-BCC. # NOTE: This must generate the same forcefield parameters as occur in `system`. [system1, topology1, positions1] = generate_openmm_system(molecule1) [system2, topology2, positions2] = generate_openmm_system(molecule2) # # Start building combined OpenMM System object. # molecule1_atoms = [ atom for atom in molecule1.GetAtoms() ] molecule2_atoms = [ atom for atom in molecule2.GetAtoms() ] molecule2_indices_in_system = dict() # Build mapping of common substructure for molecule 2. for atom2 in common2: molecule2_indices_in_system[atom2] = molecule1_indices_in_system[mapping2[atom2]] # Find residue for molecule1. residue = None for atom in topology.atoms(): if atom.index in molecule1_indices_in_system: residue = atom.residue break # Handle additional particles. print "Adding particles from system2..." for atom2 in unique2: atom = molecule2_atoms[atom2] name = atom.GetName() atomic_number = atom.GetAtomicNum() element = app.Element.getByAtomicNumber(atomic_number) mass = system2.getParticleMass(atom2) print [name, element, mass] index = system.addParticle(mass) molecule2_indices_in_system[atom2] = index # TODO: Add new atoms to topology object as well. topology.addAtom(name, element, residue) # Turn molecule2_indices_in_system into list molecule2_indices_in_system = [ molecule2_indices_in_system[atom2] for atom2 in range(molecule2.NumAtoms()) ] print "Atom mappings into System object" print "molecule1: %s" % str(molecule1_indices_in_system) print "molecule2: %s" % str(molecule2_indices_in_system) # Handle constraints. # TODO: What happens if constraints change? Raise Exception then. print "Adding constraints from system2..." for index in range(system2.getNumConstraints()): # Extract constraint distance from system2. [atom2_i, atom2_j, distance] = system.getConstraintParameters(index) # Map atoms from system2 into system. atom_i = molecule2_indices_in_system[atom2_i] atom_j = molecule2_indices_in_system[atom2_j] # Add constraint to system. system.addConstraint(atom_i, atom_j, distance) # Create new positions array. natoms = positions.shape[0] + len(unique2) # new number of atoms positions = unit.Quantity(np.resize(positions/positions.unit, [natoms,3]), positions.unit) for atom2 in unique2: (x, y, z) = molecule2.GetCoords(molecule2_atoms[atom2]) index = molecule2_indices_in_system[atom2] positions[index,0] = x * unit.angstrom positions[index,1] = y * unit.angstrom positions[index,2] = z * unit.angstrom # Build a list of Force objects in system. forces = [ system.getForce(index) for index in range(system.getNumForces()) ] forces1 = { system1.getForce(index).__class__.__name__ : system1.getForce(index) for index in range(system1.getNumForces()) } forces2 = { system2.getForce(index).__class__.__name__ : system2.getForce(index) for index in range(system2.getNumForces()) } # Process forces. for force in forces: # Get force name. force_name = force.__class__.__name__ force1 = forces1[force_name] force2 = forces2[force_name] print force_name if force_name == 'HarmonicBondForce': # # Process HarmonicBondForce # # Create index of bonds in system, system1, and system2. def unique(*args): if args[0] > args[-1]: return tuple(reversed(args)) else: return tuple(args) def index_bonds(force): bonds = dict() for index in range(force.getNumBonds()): [atom_i, atom_j, length, K] = force.getBondParameters(index) key = unique(atom_i, atom_j) # unique tuple, possibly in reverse order bonds[key] = index return bonds bonds = index_bonds(force) # index of bonds for system bonds1 = index_bonds(force1) # index of bonds for system1 bonds2 = index_bonds(force2) # index of bonds for system2 # Find bonds that are unique to each molecule. print "Finding bonds unique to each molecule..." unique_bonds1 = [ bonds1[atoms] for atoms in bonds1 if not set(atoms).issubset(common1) ] unique_bonds2 = [ bonds2[atoms] for atoms in bonds2 if not set(atoms).issubset(common2) ] # Build list of bonds shared among all molecules. print "Building a list of shared bonds..." shared_bonds = list() for atoms2 in bonds2: if set(atoms2).issubset(common2): atoms = tuple(molecule2_indices_in_system[atom2] for atom2 in atoms2) atoms1 = tuple(mapping2[atom2] for atom2 in atoms2) # Find bond index terms. index = bonds[unique(*atoms)] index1 = bonds1[unique(*atoms1)] index2 = bonds2[unique(*atoms2)] # Store. shared_bonds.append( (index, index1, index2) ) # Add bonds that are unique to molecule2. print "Adding bonds unique to molecule2..." for index2 in unique_bonds2: [atom2_i, atom2_j, length2, K2] = force2.getBondParameters(index2) atom_i = molecule2_indices_in_system[atom2_i] atom_j = molecule2_indices_in_system[atom2_j] force.addBond(atom_i, atom_j, length2, K2) # Create a CustomBondForce to handle interpolated bond parameters. print "Creating CustomBondForce..." energy_expression = '(K/2)*(r-length)^2;' energy_expression += 'K = (1-lambda)*K1 + lambda*K2;' # linearly interpolate spring constant energy_expression += 'length = (1-lambda)*length1 + lambda*length2;' # linearly interpolate bond length custom_force = mm.CustomBondForce(energy_expression) custom_force.addGlobalParameter('lambda', 0.0) custom_force.addPerBondParameter('length1') # molecule1 bond length custom_force.addPerBondParameter('K1') # molecule1 spring constant custom_force.addPerBondParameter('length2') # molecule2 bond length custom_force.addPerBondParameter('K2') # molecule2 spring constant system.addForce(custom_force) # Process bonds that are shared by molecule1 and molecule2. print "Translating shared bonds to CustomBondForce..." for (index, index1, index2) in shared_bonds: # Zero out standard bond force. [atom_i, atom_j, length, K] = force.getBondParameters(index) force.setBondParameters(index, atom_i, atom_j, length, K*0.0) # Create interpolated bond parameters. [atom1_i, atom1_j, length1, K1] = force1.getBondParameters(index1) [atom2_i, atom2_j, length2, K2] = force2.getBondParameters(index2) custom_force.addBond(atom_i, atom_j, [length1, K1, length2, K2]) if force_name == 'HarmonicAngleForce': # # Process HarmonicAngleForce # # Create index of angles in system, system1, and system2. def unique(*args): if args[0] > args[-1]: return tuple(reversed(args)) else: return tuple(args) def index_angles(force): angles = dict() for index in range(force.getNumAngles()): [atom_i, atom_j, atom_k, angle, K] = force.getAngleParameters(index) key = unique(atom_i, atom_j, atom_k) # unique tuple, possibly in reverse order angles[key] = index return angles angles = index_angles(force) # index of angles for system angles1 = index_angles(force1) # index of angles for system1 angles2 = index_angles(force2) # index of angles for system2 # Find angles that are unique to each molecule. print "Finding angles unique to each molecule..." unique_angles1 = [ angles1[atoms] for atoms in angles1 if not set(atoms).issubset(common1) ] unique_angles2 = [ angles2[atoms] for atoms in angles2 if not set(atoms).issubset(common2) ] # Build list of angles shared among all molecules. print "Building a list of shared angles..." shared_angles = list() for atoms2 in angles2: if set(atoms2).issubset(common2): atoms = tuple(molecule2_indices_in_system[atom2] for atom2 in atoms2) atoms1 = tuple(mapping2[atom2] for atom2 in atoms2) # Find angle index terms. index = angles[unique(*atoms)] index1 = angles1[unique(*atoms1)] index2 = angles2[unique(*atoms2)] # Store. shared_angles.append( (index, index1, index2) ) # Add angles that are unique to molecule2. print "Adding angles unique to molecule2..." for index2 in unique_angles2: [atom2_i, atom2_j, atom2_k, theta2, K2] = force2.getAngleParameters(index2) atom_i = molecule2_indices_in_system[atom2_i] atom_j = molecule2_indices_in_system[atom2_j] atom_k = molecule2_indices_in_system[atom2_k] force.addAngle(atom_i, atom_j, atom_k, theta2, K2) # Create a CustomAngleForce to handle interpolated angle parameters. print "Creating CustomAngleForce..." energy_expression = '(K/2)*(theta-theta0)^2;' energy_expression += 'K = (1-lambda)*K_1 + lambda*K_2;' # linearly interpolate spring constant energy_expression += 'theta0 = (1-lambda)*theta0_1 + lambda*theta0_2;' # linearly interpolate equilibrium angle custom_force = mm.CustomAngleForce(energy_expression) custom_force.addGlobalParameter('lambda', 0.0) custom_force.addPerAngleParameter('theta0_1') # molecule1 equilibrium angle custom_force.addPerAngleParameter('K_1') # molecule1 spring constant custom_force.addPerAngleParameter('theta0_2') # molecule2 equilibrium angle custom_force.addPerAngleParameter('K_2') # molecule2 spring constant system.addForce(custom_force) # Process angles that are shared by molecule1 and molecule2. print "Translating shared angles to CustomAngleForce..." for (index, index1, index2) in shared_angles: # Zero out standard angle force. [atom_i, atom_j, atom_k, theta0, K] = force.getAngleParameters(index) force.setAngleParameters(index, atom_i, atom_j, atom_k, theta0, K*0.0) # Create interpolated angle parameters. [atom1_i, atom1_j, atom1_k, theta1, K1] = force1.getAngleParameters(index1) [atom2_i, atom2_j, atom2_k, theta2, K2] = force2.getAngleParameters(index2) custom_force.addAngle(atom_i, atom_j, atom_k, [theta1, K1, theta2, K2]) if force_name == 'PeriodicTorsionForce': # # Process PeriodicTorsionForce # TODO: Match up periodicities and deal with multiple terms per torsion # # Create index of torsions in system, system1, and system2. def unique(*args): if args[0] > args[-1]: return tuple(reversed(args)) else: return tuple(args) def index_torsions(force): torsions = dict() for index in range(force.getNumTorsions()): [atom_i, atom_j, atom_k, atom_l, periodicity, phase, K] = force.getTorsionParameters(index) key = unique(atom_i, atom_j, atom_k, atom_l) # unique tuple, possibly in reverse order torsions[key] = index return torsions torsions = index_torsions(force) # index of torsions for system torsions1 = index_torsions(force1) # index of torsions for system1 torsions2 = index_torsions(force2) # index of torsions for system2 # Find torsions that are unique to each molecule. print "Finding torsions unique to each molecule..." unique_torsions1 = [ torsions1[atoms] for atoms in torsions1 if not set(atoms).issubset(common1) ] unique_torsions2 = [ torsions2[atoms] for atoms in torsions2 if not set(atoms).issubset(common2) ] # Build list of torsions shared among all molecules. print "Building a list of shared torsions..." shared_torsions = list() for atoms2 in torsions2: if set(atoms2).issubset(common2): atoms = tuple(molecule2_indices_in_system[atom2] for atom2 in atoms2) atoms1 = tuple(mapping2[atom2] for atom2 in atoms2) # Find torsion index terms. try: index = torsions[unique(*atoms)] index1 = torsions1[unique(*atoms1)] index2 = torsions2[unique(*atoms2)] except Exception as e: print e print "torsions : %s" % str(unique(*atoms)) print "torsions1: %s" % str(unique(*atoms1)) print "torsions2: %s" % str(unique(*atoms2)) raise Exception("Error occurred in building a list of torsions common to all molecules.") # Store. shared_torsions.append( (index, index1, index2) ) # Add torsions that are unique to molecule2. print "Adding torsions unique to molecule2..." for index2 in unique_torsions2: [atom2_i, atom2_j, atom2_k, atom2_l, periodicity2, phase2, K2] = force2.getTorsionParameters(index2) atom_i = molecule2_indices_in_system[atom2_i] atom_j = molecule2_indices_in_system[atom2_j] atom_k = molecule2_indices_in_system[atom2_k] atom_l = molecule2_indices_in_system[atom2_l] force.addTorsion(atom_i, atom_j, atom_k, atom_l, periodicity2, phase2, K2) # Create a CustomTorsionForce to handle interpolated torsion parameters. print "Creating CustomTorsionForce..." energy_expression = '(1-lambda)*U1 + lambda*U2;' energy_expression += 'U1 = K1*(1+cos(periodicity1*theta-phase1));' energy_expression += 'U2 = K2*(1+cos(periodicity2*theta-phase2));' custom_force = mm.CustomTorsionForce(energy_expression) custom_force.addGlobalParameter('lambda', 0.0) custom_force.addPerTorsionParameter('periodicity1') # molecule1 periodicity custom_force.addPerTorsionParameter('phase1') # molecule1 phase custom_force.addPerTorsionParameter('K1') # molecule1 spring constant custom_force.addPerTorsionParameter('periodicity2') # molecule2 periodicity custom_force.addPerTorsionParameter('phase2') # molecule2 phase custom_force.addPerTorsionParameter('K2') # molecule2 spring constant system.addForce(custom_force) # Process torsions that are shared by molecule1 and molecule2. print "Translating shared torsions to CustomTorsionForce..." for (index, index1, index2) in shared_torsions: # Zero out standard torsion force. [atom_i, atom_j, atom_k, atom_l, periodicity, phase, K] = force.getTorsionParameters(index) force.setTorsionParameters(index, atom_i, atom_j, atom_k, atom_l, periodicity, phase, K*0.0) # Create interpolated torsion parameters. [atom1_i, atom1_j, atom1_k, atom1_l, periodicity1, phase1, K1] = force1.getTorsionParameters(index1) [atom2_i, atom2_j, atom2_k, atom2_l, periodicity2, phase2, K2] = force2.getTorsionParameters(index2) custom_force.addTorsion(atom_i, atom_j, atom_k, atom_l, [periodicity1, phase1, K1, periodicity2, phase2, K2]) if force_name == 'NonbondedForce': # # Process NonbondedForce # # Add nonbonded entries for molecule2 to ensure total number of particle entries is correct. for atom in unique2: [charge, sigma, epsilon] = force2.getParticleParameters(atom) force.addParticle(charge, sigma, epsilon) # Zero out nonbonded entries for molecule1. for atom in molecule1_indices_in_system: [charge, sigma, epsilon] = force.getParticleParameters(atom) force.setParticleParameters(atom, 0*charge, sigma, 0*epsilon) # Zero out nonbonded entries for molecule2. for atom in molecule2_indices_in_system: [charge, sigma, epsilon] = force.getParticleParameters(atom) force.setParticleParameters(atom, 0*charge, sigma, 0*epsilon) # Create index of exceptions in system, system1, and system2. def unique(*args): if args[0] > args[-1]: return tuple(reversed(args)) else: return tuple(args) def index_exceptions(force): exceptions = dict() for index in range(force.getNumExceptions()): [atom_i, atom_j, chargeProd, sigma, epsilon] = force.getExceptionParameters(index) key = unique(atom_i, atom_j) # unique tuple, possibly in reverse order exceptions[key] = index return exceptions exceptions = index_exceptions(force) # index of exceptions for system exceptions1 = index_exceptions(force1) # index of exceptions for system1 exceptions2 = index_exceptions(force2) # index of exceptions for system2 # Find exceptions that are unique to each molecule. print "Finding exceptions unique to each molecule..." unique_exceptions1 = [ exceptions1[atoms] for atoms in exceptions1 if not set(atoms).issubset(common1) ] unique_exceptions2 = [ exceptions2[atoms] for atoms in exceptions2 if not set(atoms).issubset(common2) ] # Build list of exceptions shared among all molecules. print "Building a list of shared exceptions..." shared_exceptions = list() for atoms2 in exceptions2: if set(atoms2).issubset(common2): atoms = tuple(molecule2_indices_in_system[atom2] for atom2 in atoms2) atoms1 = tuple(mapping2[atom2] for atom2 in atoms2) # Find exception index terms. index = exceptions[unique(*atoms)] index1 = exceptions1[unique(*atoms1)] index2 = exceptions2[unique(*atoms2)] # Store. shared_exceptions.append( (index, index1, index2) ) # Add exceptions that are unique to molecule2. print "Adding exceptions unique to molecule2..." for index2 in unique_exceptions2: [atom2_i, atom2_j, chargeProd, sigma, epsilon] = force2.getExceptionParameters(index2) atom_i = molecule2_indices_in_system[atom2_i] atom_j = molecule2_indices_in_system[atom2_j] force.addException(atom_i, atom_j, chargeProd, sigma, epsilon) # Create list of alchemically modified atoms in system. alchemical_atom_indices = list(set(molecule1_indices_in_system).union(set(molecule2_indices_in_system))) # Create atom groups. natoms = system.getNumParticles() atomset1 = set(alchemical_atom_indices) # only alchemically-modified atoms atomset2 = set(range(system.getNumParticles())) # all atoms, including alchemical region # CustomNonbondedForce energy expression. sterics_energy_expression = "" electrostatics_energy_expression = "" # Create a CustomNonbondedForce to handle alchemically interpolated nonbonded parameters. # Select functional form based on nonbonded method. method = force.getNonbondedMethod() if method in [mm.NonbondedForce.NoCutoff]: # soft-core Lennard-Jones sterics_energy_expression += "U_sterics = 4*epsilon*x*(x-1.0); x1 = (sigma/reff_sterics)^6;" # soft-core Coulomb electrostatics_energy_expression += "U_electrostatics = ONE_4PI_EPS0*chargeprod/reff_electrostatics;" elif method in [mm.NonbondedForce.CutoffPeriodic, mm.NonbondedForce.CutoffNonPeriodic]: # soft-core Lennard-Jones sterics_energy_expression += "U_sterics = 4*epsilon*x*(x-1.0); x = (sigma/reff_sterics)^6;" # reaction-field electrostatics epsilon_solvent = force.getReactionFieldDielectric() r_cutoff = force.getCutoffDistance() electrostatics_energy_expression += "U_electrostatics = ONE_4PI_EPS0*chargeprod*(reff_electrostatics^(-1) + k_rf*reff_electrostatics^2 - c_rf);" k_rf = r_cutoff**(-3) * ((epsilon_solvent - 1) / (2*epsilon_solvent + 1)) c_rf = r_cutoff**(-1) * ((3*epsilon_solvent) / (2*epsilon_solvent + 1)) electrostatics_energy_expression += "k_rf = %f;" % (k_rf / k_rf.in_unit_system(unit.md_unit_system).unit) electrostatics_energy_expression += "c_rf = %f;" % (c_rf / c_rf.in_unit_system(unit.md_unit_system).unit) elif method in [mm.NonbondedForce.PME, mm.NonbondedForce.Ewald]: # soft-core Lennard-Jones sterics_energy_expression += "U_sterics = 4*epsilon*x*(x-1.0); x = (sigma/reff_sterics)^6;" # Ewald direct-space electrostatics [alpha_ewald, nx, ny, nz] = force.getPMEParameters() if alpha_ewald == 0.0: # If alpha is 0.0, alpha_ewald is computed by OpenMM from from the error tolerance. delta = force.getEwaldErrorTolerance() r_cutoff = force.getCutoffDistance() alpha_ewald = np.sqrt(-np.log(2*delta)) / r_cutoff electrostatics_energy_expression += "U_electrostatics = ONE_4PI_EPS0*chargeprod*erfc(alpha_ewald*reff_electrostatics)/reff_electrostatics;" electrostatics_energy_expression += "alpha_ewald = %f;" % (alpha_ewald / alpha_ewald.in_unit_system(unit.md_unit_system).unit) # TODO: Handle reciprocal-space electrostatics else: raise Exception("Nonbonded method %s not supported yet." % str(method)) # Add additional definitions common to all methods. sterics_energy_expression += "epsilon = (1-lambda)*epsilonA + lambda*epsilonB;" #interpolation sterics_energy_expression += "reff_sterics = sigma*((softcore_alpha*lambda_alpha + (r/sigma)^6))^(1/6);" # effective softcore distance for sterics sterics_energy_expression += "softcore_alpha = %f;" % softcore_alpha # TODO: We may have to ensure that softcore_degree is 1 if we are close to an alchemically-eliminated endpoint. sterics_energy_expression += "lambda_alpha = lambda*(1-lambda);" electrostatics_energy_expression += "chargeProd = (1-lambda)*chargeProdA + lambda*chargeProdB;" #interpolation electrostatics_energy_expression += "reff_electrostatics = sqrt(softcore_beta*lambda_beta + r^2);" # effective softcore distance for electrostatics electrostatics_energy_expression += "softcore_beta = %f;" % (softcore_beta / softcore_beta.in_unit_system(unit.md_unit_system).unit) electrostatics_energy_expression += "ONE_4PI_EPS0 = %f;" % ONE_4PI_EPS0 # already in OpenMM units # TODO: We may have to ensure that softcore_degree is 1 if we are close to an alchemically-eliminated endpoint. sterics_energy_expression += "lambda_beta = lambda*(1-lambda);" # Define mixing rules. sterics_mixing_rules = "" sterics_mixing_rules += "epsilonA = sqrt(epsilonA1*epsilonA2);" # mixing rule for epsilon sterics_mixing_rules += "epsilonB = sqrt(epsilonB1*epsilonB2);" # mixing rule for epsilon sterics_mixing_rules += "sigmaA = 0.5*(sigmaA1 + sigmaA2);" # mixing rule for sigma sterics_mixing_rules += "sigmaB = 0.5*(sigmaB1 + sigmaB2);" # mixing rule for sigma electrostatics_mixing_rules = "" electrostatics_mixing_rules += "chargeprodA = chargeA1*chargeA2;" # mixing rule for charges electrostatics_mixing_rules += "chargeprodB = chargeB1*chargeB2;" # mixing rule for charges # Create CustomNonbondedForce to handle interactions between alchemically-modified atoms and rest of system. electrostatics_custom_nonbonded_force = mm.CustomNonbondedForce("U_electrostatics;" + electrostatics_energy_expression + electrostatics_mixing_rules) electrostatics_custom_nonbonded_force.addGlobalParameter("lambda", 0.0); electrostatics_custom_nonbonded_force.addPerParticleParameter("chargeA") # partial charge initial electrostatics_custom_nonbonded_force.addPerParticleParameter("chargeB") # partial charge final sterics_custom_nonbonded_force = mm.CustomNonbondedForce("U_sterics;" + sterics_energy_expression + sterics_mixing_rules) sterics_custom_nonbonded_force.addGlobalParameter("lambda", 0.0); sterics_custom_nonbonded_force.addPerParticleParameter("sigmaA") # Lennard-Jones sigma initial sterics_custom_nonbonded_force.addPerParticleParameter("epsilonA") # Lennard-Jones epsilon initial sterics_custom_nonbonded_force.addPerParticleParameter("sigmaB") # Lennard-Jones sigma final sterics_custom_nonbonded_force.addPerParticleParameter("epsilonB") # Lennard-Jones epsilon final # Restrict interaction evaluation to be between alchemical atoms and rest of environment. # TODO: Exclude intra-alchemical region if we are separately handling that through a separate CustomNonbondedForce for decoupling. sterics_custom_nonbonded_force.addInteractionGroup(atomset1, atomset2) electrostatics_custom_nonbonded_force.addInteractionGroup(atomset1, atomset2) # Add exclusions between unique parts of molecule1 and molecule2 so they do not interact. print "Add exclusions between unique parts of molecule1 and molecule2 that should not interact..." for atom1_i in unique1: for atom2_j in unique2: atom_i = molecule1_indices_in_system[atom1_i] atom_j = molecule2_indices_in_system[atom2_j] electrostatics_custom_nonbonded_force.addExclusion(atom_i, atom_j) sterics_custom_nonbonded_force.addExclusion(atom_i, atom_j) # Add custom forces to system. system.addForce(sterics_custom_nonbonded_force) system.addForce(electrostatics_custom_nonbonded_force) # Create CustomBondForce to handle exceptions for both kinds of interactions. #custom_bond_force = mm.CustomBondForce("U_sterics + U_electrostatics;" + sterics_energy_expression + electrostatics_energy_expression) #custom_bond_force.addGlobalParameter("lambda", 0.0); #custom_bond_force.addPerBondParameter("chargeprodA") # charge product #custom_bond_force.addPerBondParameter("sigmaA") # Lennard-Jones effective sigma #custom_bond_force.addPerBondParameter("epsilonA") # Lennard-Jones effective epsilon #custom_bond_force.addPerBondParameter("chargeprodB") # charge product #custom_bond_force.addPerBondParameter("sigmaB") # Lennard-Jones effective sigma #custom_bond_force.addPerBondParameter("epsilonB") # Lennard-Jones effective epsilon #system.addForce(custom_bond_force) # Copy over all Nonbonded parameters for normal atoms to Custom*Force objects. for particle_index in range(force.getNumParticles()): # Retrieve parameters. [charge, sigma, epsilon] = force.getParticleParameters(particle_index) # Add parameters to custom force handling interactions between alchemically-modified atoms and rest of system. sterics_custom_nonbonded_force.addParticle([sigma, epsilon, sigma, epsilon]) electrostatics_custom_nonbonded_force.addParticle([charge, charge]) # Copy over parameters for common substructure. for atom1 in common1: atom2 = mapping1[atom1] # index into system2 index = molecule1_indices_in_system[atom1] # index into system [charge1, sigma1, epsilon1] = force1.getParticleParameters(atom1) [charge2, sigma2, epsilon2] = force2.getParticleParameters(atom2) sterics_custom_nonbonded_force.setParticleParameters(index, [sigma1, epsilon1, sigma2, epsilon2]) electrostatics_custom_nonbonded_force.setParticleParameters(index, [charge1, charge2]) # Copy over parameters for molecule1 unique atoms. for atom1 in unique1: index = molecule1_indices_in_system[atom1] # index into system [charge1, sigma1, epsilon1] = force1.getParticleParameters(atom1) sterics_custom_nonbonded_force.setParticleParameters(index, [sigma1, epsilon1, sigma1, 0*epsilon1]) electrostatics_custom_nonbonded_force.setParticleParameters(index, [charge1, 0*charge1]) # Copy over parameters for molecule2 unique atoms. for atom2 in unique2: index = molecule2_indices_in_system[atom2] # index into system [charge2, sigma2, epsilon2] = force2.getParticleParameters(atom2) sterics_custom_nonbonded_force.setParticleParameters(index, [sigma2, 0*epsilon2, sigma2, epsilon2]) electrostatics_custom_nonbonded_force.setParticleParameters(index, [0*charge2, charge2]) else: #raise Exception("Force type %s unknown." % force_name) pass return [system, topology, positions]
def calc_rms(self, fitmol): fit_match = self.get_match(fitmol) match = oechem.OEMatch() for rm, fm in zip(self.ref_match, fit_match): match.AddPair(rm, fm) return oechem.OERMSD(self.refmol, fitmol, match, False)