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 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 normalize_coordinates(mol, dih): """Reset the coordinates of an OEGraphMol object with respect to a dihedral. The first three atoms of the dihedral will be set to the following coordinates: |Atom| Position | |----|----------| | 0 | (0,0,0) | | 1 | (x1,0,0) | | 2 | (x2,y2,0)| This defines the coordinate system required to determine the coordinates of all the other atoms in the fragment. Arguments: mol: An OEGraphMol object. dih: An OEAtomBondSet with atoms belonging to mol. Returns: success: A bool indicating whether the transformation was successful. """ dih_atoms = [atom for atom in dih.GetAtoms()] # Translate the fragment until the first atom of the dihedral is at the origin origin = mol.GetCoords()[dih_atoms[0].GetIdx()] shift = oechem.OEDoubleArray([-x for x in origin]) oechem.OETranslate(mol, shift) # Get coordinates of atoms 2 and 3 in the dihedral u = np.array(mol.GetCoords()[dih_atoms[1].GetIdx()]) v = np.array(mol.GetCoords()[dih_atoms[2].GetIdx()]) # Get three orthogonal unit vectors u_hat = u / np.linalg.norm(u) v_hat = v - ( (np.dot(u, v) * u) / np.dot(u, u)) # Gram-Schmidt Orthogonalization v_hat = v_hat / np.linalg.norm(v_hat) w_hat = np.cross(u_hat, v_hat) # Assemble unit vectors into rotation matrix R = np.stack((u_hat, v_hat, w_hat), axis=0) rotation_matrix = oechem.OEDoubleArray(R.flatten()) oechem.OERotate(mol, rotation_matrix)
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 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 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 pull_close(receptor_oemol, ligand_oemol, min_bound, max_bound): """Heuristic algorithm to quickly translate the ligand close to the receptor. The distance of the ligand from the receptor here is defined as the shortest Euclidean distance between an atom of the ligand and one of the receptor. The molecules positions will not be modified if the ligand is already at a distance in the interval [min_bound, max_bound]. Parameters ---------- receptor_oemol : openeye.oechem The receptor molecule. ligand_oemol : openeye.oechem The ligand molecule to push close to the receptor. min_bound : float Minimum distance from the receptor to the ligand. This should be high enough for the ligand to not overlap the receptor atoms at the beginning of the simulation. max_bound : float Maximum distance from the receptor to the ligand. This should be short enough to make the ligand and the receptor interact since the beginning of the simulation. """ goal_distance = (min_bound + max_bound) / 2 # Get all the ligand and receptor atoms position oe_coords = oechem.OEFloatArray(3) ligand_pos = np.zeros((ligand_oemol.NumAtoms(), 3)) for i, atom in enumerate(ligand_oemol.GetAtoms()): ligand_oemol.GetCoords(atom, oe_coords) ligand_pos[i] = oe_coords receptor_pos = np.zeros((receptor_oemol.NumAtoms(), 3)) for i, atom in enumerate(receptor_oemol.GetAtoms()): receptor_oemol.GetCoords(atom, oe_coords) receptor_pos[i] = oe_coords # Find translation final_translation = np.zeros(3) while True: # Compute squared distances # Each row is an array of distances from a ligand atom to all receptor atoms # We don't need to apply square root to everything distances2 = np.array([((receptor_pos - ligand_pos[i])**2).sum(1) for i in xrange(len(ligand_pos))]) # Find closest atoms and their distance min_idx = np.unravel_index(distances2.argmin(), distances2.shape) min_dist = np.sqrt(distances2[min_idx]) # If closest atom is between boundaries translate ligand if min_bound <= min_dist <= max_bound: break # Compute unit vector that connects receptor and ligand atom direction = receptor_pos[min_idx[1]] - ligand_pos[min_idx[0]] direction = direction / np.sqrt((direction**2).sum()) if max_bound < min_dist: # the atom is far away translation = (min_dist - goal_distance) * direction ligand_pos += translation final_translation += translation elif min_dist < min_bound: # the two molecules overlap max_dist = np.sqrt(distances2.max()) translation = (max_dist + goal_distance) * direction ligand_pos += translation final_translation += translation # Translate OEChem molecule translation_oe = oechem.OEDoubleArray(final_translation) oechem.OETranslate(ligand_oemol, translation_oe)
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]