diff --git a/.travis.yml b/.travis.yml index 6a09d19..dfda84d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,7 +45,7 @@ script: - conda install --yes --use-local ${PACKAGENAME}-dev # Install testing dependencies - conda install --yes --quiet nose nose-timer - - conda install --yes --quiet openmmtools + - conda install --yes --quiet openmmtools - conda install --yes --quiet packmol - conda install --yes --quiet rdkit # Test the package diff --git a/openmoltools/openeye.py b/openmoltools/openeye.py index 0ee1135..1ca0ede 100644 --- a/openmoltools/openeye.py +++ b/openmoltools/openeye.py @@ -10,7 +10,8 @@ # Note: We recommend having every function return *copies* of input, to avoid headaches associated with in-place changes -def get_charges(molecule, max_confs=800, strictStereo=True, normalize=True, keep_confs=None): +def get_charges(molecule, max_confs=800, strictStereo=True, + normalize=True, keep_confs=None, legacy=True): """Generate charges for an OpenEye OEMol molecule. Parameters @@ -34,6 +35,9 @@ def get_charges(molecule, max_confs=800, strictStereo=True, normalize=True, keep Otherwise, keep_confs = N will return an OEMol with up to N generated conformations. Multiple conformations are still used to *determine* the charges. + legacy : bool, default=True + If False, uses the new OpenEye charging engine. + See https://docs.eyesopen.com/toolkits/python/quacpactk/OEProtonFunctions/OEAssignCharges.html# Returns ------- @@ -62,10 +66,16 @@ def get_charges(molecule, max_confs=800, strictStereo=True, normalize=True, keep charged_copy = generate_conformers(molecule, max_confs=max_confs, strictStereo=strictStereo) # Generate up to max_confs conformers - status = oequacpac.OEAssignPartialCharges(charged_copy, oequacpac.OECharges_AM1BCCSym) # AM1BCCSym recommended by Chris Bayly to KAB+JDC, Oct. 20 2014. + if not legacy: + # 2017.2.1 OEToolkits new charging function + status = oequacpac.OEAssignCharges(charged_copy, oequacpac.OEAM1BCCCharges()) + if not status: raise(RuntimeError("OEAssignCharges failed.")) + else: + # AM1BCCSym recommended by Chris Bayly to KAB+JDC, Oct. 20 2014. + status = oequacpac.OEAssignPartialCharges(charged_copy, oequacpac.OECharges_AM1BCCSym) + if not status: raise(RuntimeError("OEAssignPartialCharges returned error code %d" % status)) + - if not status: - raise(RuntimeError("OEAssignPartialCharges returned error code %d" % status)) #Determine conformations to return if keep_confs == None: @@ -78,6 +88,8 @@ def get_charges(molecule, max_confs=800, strictStereo=True, normalize=True, keep #Copy coordinates to single conformer charged_copy.SetCoords( original ) elif keep_confs > 0: + logger.debug("keep_confs was set to %s. Molecule positions will be reset." % keep_confs) + #Otherwise if a number is provided, return this many confs if available for k, conf in enumerate( charged_copy.GetConfs() ): if k > keep_confs - 1: diff --git a/openmoltools/tests/test_openeye.py b/openmoltools/tests/test_openeye.py index de1fdf1..708b496 100644 --- a/openmoltools/tests/test_openeye.py +++ b/openmoltools/tests/test_openeye.py @@ -50,7 +50,7 @@ def test_butanol_unnormalized(): m0 = openmoltools.openeye.iupac_to_oemol("butanol") m0.SetTitle("MyCustomTitle") m1 = openmoltools.openeye.get_charges(m0, normalize=False, keep_confs=1) - eq(m0.NumAtoms(), m1.NumAtoms()) + eq(m0.NumAtoms(), m1.NumAtoms()) assert m1.NumConfs() == 1, "This OEMol was created to have a single conformation." assert m1.NumAtoms() == 15, "Butanol should have 15 atoms" assert m0.GetTitle() == m1.GetTitle(), "The title of the molecule should not be changed by normalization." @@ -91,19 +91,19 @@ def test_butanol(): eq(m0.NumAtoms(), m1.NumAtoms()) assert m1.NumConfs() >= 2, "Butanol should have multiple conformers." assert m1.NumAtoms() == 15, "Butanol should have 15 atoms" - + all_data = {} for k, molecule in enumerate(m1.GetConfs()): names_to_charges, str_repr = openmoltools.openeye.get_names_to_charges(molecule) all_data[k] = names_to_charges eq(sum(names_to_charges.values()), 0.0, decimal=7) # Net charge should be zero - + # Build a table of charges indexed by conformer number and atom name all_data = pd.DataFrame(all_data) - + # The standard deviation along the conformer axis should be zero if all conformers have same charges eq(all_data.std(1).values, np.zeros(m1.NumAtoms()), decimal=7) - + with utils.enter_temp_directory(): # Try saving to disk as mol2 openmoltools.openeye.molecule_to_mol2(m1, "out.mol2") @@ -128,10 +128,10 @@ def test_benzene(): print(m1.NumConfs()) assert m1.NumConfs() == 1, "Benezene should have 1 conformer" assert m1.NumAtoms() == 12, "Benezene should have 12 atoms" - + names_to_charges, str_repr = openmoltools.openeye.get_names_to_charges(m1) eq(sum(names_to_charges.values()), 0.0, decimal=7) # Net charge should be zero - + with utils.enter_temp_directory(): # Try saving to disk as mol2 openmoltools.openeye.molecule_to_mol2(m1, "out.mol2") @@ -149,7 +149,7 @@ def test_benzene(): @skipIf(not HAVE_OE, "Cannot test openeye module without OpenEye tools.") -def test_link_in_utils(): +def test_link_in_utils(): m0 = openmoltools.openeye.iupac_to_oemol("benzene") m1 = openmoltools.openeye.get_charges(m0) with utils.enter_temp_directory(): @@ -170,7 +170,7 @@ def test_smiles(): @skipIf(not HAVE_OE, "Cannot test openeye module without OpenEye tools.") def test_ffxml(): - with utils.enter_temp_directory(): + with utils.enter_temp_directory(): m0 = openmoltools.openeye.smiles_to_oemol("CCCCO") charged0 = openmoltools.openeye.get_charges(m0) m1 = openmoltools.openeye.smiles_to_oemol("ClC(Cl)(Cl)Cl") @@ -182,7 +182,7 @@ def test_ffxml(): @skipIf(not HAVE_OE, "Cannot test openeye module without OpenEye tools.") def test_ffxml_simulation(): """Test converting toluene and benzene smiles to oemol to ffxml to openmm simulation.""" - with utils.enter_temp_directory(): + with utils.enter_temp_directory(): m0 = openmoltools.openeye.smiles_to_oemol("Cc1ccccc1") charged0 = openmoltools.openeye.get_charges(m0) m1 = openmoltools.openeye.smiles_to_oemol("c1ccccc1") @@ -208,11 +208,11 @@ def test_ffxml_simulation(): for k, ligand in enumerate(ligands): ligand_traj = trajectories[k] ligand_traj.center_coordinates() - + eq(ligand_traj.n_atoms, n_atoms[k]) eq(ligand_traj.n_frames, 1) - #Move the pre-centered ligand sufficiently far away from the protein to avoid a clash. + #Move the pre-centered ligand sufficiently far away from the protein to avoid a clash. min_atom_pair_distance = ((ligand_traj.xyz[0] ** 2.).sum(1) ** 0.5).max() + ((protein_traj.xyz[0] ** 2.).sum(1) ** 0.5).max() + 0.3 ligand_traj.xyz += np.array([1.0, 0.0, 0.0]) * min_atom_pair_distance @@ -252,7 +252,7 @@ def test_charge_fail2(): @skipIf(not HAVE_OE, "Cannot test openeye module without OpenEye tools.") def test_charge_success1(): - with utils.enter_temp_directory(): + with utils.enter_temp_directory(): openmoltools.openeye.smiles_to_antechamber(smiles_fails_with_strictStereo, "test.mol2", "test.frcmod", strictStereo=False) @@ -261,6 +261,18 @@ def test_charge_success2(): m = openmoltools.openeye.smiles_to_oemol(smiles_fails_with_strictStereo) m = openmoltools.openeye.get_charges(m, strictStereo=False) +@skipIf(not HAVE_OE, "Cannot test openeye module without OpenEye tools.") +@raises(RuntimeError) +def test_oeassigncharges_fail(): + # Fail test for OEToolkits (2017.2.1) new charging function + m = openmoltools.openeye.smiles_to_oemol(smiles_fails_with_strictStereo) + m = openmoltools.openeye.get_charges(m, strictStereo=False, legacy=False) + +@skipIf(not HAVE_OE, "Cannot test openeye module without OpenEye tools.") +def test_oeassigncharges_success(): + # Success test for OEToolkits (2017.2.1) new charging function + m = openmoltools.openeye.iupac_to_oemol("butanol") + m = openmoltools.openeye.get_charges(m, legacy=False) @skipIf(not HAVE_OE, "Cannot test openeye module without OpenEye tools.") @skipIf(not HAVE_PARMED, "Cannot test without Parmed Chemistry.")