def test_parameter_round_trip(method, tmpdir): """ Check we can parametrise a molecule then write out the same parameters. """ with tmpdir.as_cwd(): mol = Ligand.from_file(get_data("acetone.sdf")) method(mol) # write out params mol.write_parameters(name="test") # make a second mol mol2 = Ligand.from_file(get_data("acetone.sdf")) XML(mol2, "test.xml") assert mol.AtomTypes == mol2.AtomTypes for bond in mol.HarmonicBondForce.keys(): assert mol.HarmonicBondForce[bond] == pytest.approx( mol2.HarmonicBondForce[bond], abs=1e-6 ) for angle in mol.HarmonicAngleForce.keys(): assert mol.HarmonicAngleForce[angle] == pytest.approx( mol2.HarmonicAngleForce[angle], abs=1e-6 ) for atom in range(mol.n_atoms): assert ( pytest.approx(mol.NonbondedForce[atom], abs=1e-6) == mol2.NonbondedForce[atom] ) for dihedral, terms in mol.PeriodicTorsionForce.items(): try: other_dih = mol2.PeriodicTorsionForce[dihedral] except KeyError: other_dih = mol2.PeriodicTorsionForce[tuple(reversed(dihedral))] assert np.allclose(terms[:4], other_dih[:4])
def test_to_rdkit(molecule): """ Make sure we can convert to rdkit. We test on bace which has a chiral center and 12-dichloroethene which has a stereo bond. """ from rdkit import Chem mol = Ligand.from_file(file_name=get_data(molecule)) rd_mol = mol.to_rdkit() # make sure the atom and bond stereo match for atom in rd_mol.GetAtoms(): qb_atom = mol.atoms[atom.GetIdx()] assert atom.GetIsAromatic() is qb_atom.aromatic if qb_atom.stereochemistry is not None: if qb_atom.stereochemistry == "S": assert atom.GetChiralTag() == Chem.CHI_TETRAHEDRAL_CCW else: assert atom.GetChiralTag() == Chem.CHI_TETRAHEDRAL_CW for bond in rd_mol.GetBonds(): qb_bond = mol.bonds[bond.GetIdx()] assert qb_bond.aromatic is bond.GetIsAromatic() assert qb_bond.bond_order == bond.GetBondTypeAsDouble() if qb_bond.stereochemistry is not None: if qb_bond.stereochemistry == "E": assert bond.GetStereo() == Chem.BondStereo.STEREOE else: assert bond.GetStereo() == Chem.BondStereo.STEREOZ
def test_no_rotatable_bonds(): """ If there are no dihedrals in the molecule make sure we return None. """ mol = Ligand.from_file(file_name=get_data("water.pdb")) assert mol.rotatable_bonds is None assert mol.n_rotatable_bonds == 0
def test_no_impropers(): """ Make sure we return None when no impropers are found in the molecule. """ mol = Ligand.from_file(file_name=get_data("water.pdb")) assert mol.improper_torsions is None assert mol.n_improper_torsions == 0
def test_no_dihedrals(): """ Make sure we return None when no dihedrals are found in the molecule. """ mol = Ligand.from_file(file_name=get_data("water.pdb")) assert mol.dihedrals is None assert mol.n_dihedrals == 0
def test_xml_sites_roundtrip(tmpdir): """ If we load in an xml with sites make sure we can write it back out. """ with tmpdir.as_cwd(): mol = Ligand.from_file(get_data("pyridine.pdb")) XML(mol, input_file=get_data("pyridine.xml")) mol.write_parameters(name="test") mol2 = Ligand.from_file(get_data("pyridine.pdb")) XML(mol2, input_file="test.xml") assert mol.AtomTypes == mol2.AtomTypes for bond in mol.HarmonicBondForce.keys(): assert ( pytest.approx(mol.HarmonicBondForce[bond], abs=1e-6) == mol2.HarmonicBondForce[bond] ) for angle in mol.HarmonicAngleForce.keys(): assert ( pytest.approx(mol.HarmonicAngleForce[angle], abs=1e-6) == mol2.HarmonicAngleForce[angle] ) for atom in range(mol.n_atoms): assert ( pytest.approx(mol.NonbondedForce[atom], abs=1e-6) == mol2.NonbondedForce[atom] ) # make sure the virtual site was round tripped mol_site = mol.extra_sites[0] mol2_site = mol2.extra_sites[0] assert mol_site.charge == mol2_site.charge assert mol_site.parent_index == mol2_site.parent_index assert mol_site.closest_a_index == mol2_site.closest_a_index assert mol_site.closest_b_index == mol2_site.closest_b_index assert mol_site.p1 == mol2_site.p1 assert mol_site.p2 == mol2_site.p2 assert mol_site.p3 == mol2_site.p3 for dihedral, terms in mol.PeriodicTorsionForce.items(): try: other_dih = mol2.PeriodicTorsionForce[dihedral] except KeyError: other_dih = mol2.PeriodicTorsionForce[tuple(reversed(dihedral))] assert np.allclose(terms[:4], other_dih[:4])
def test_ligand_from_file(file_name): """ For the given file type make sure rdkit can parse it and return the molecule. """ mol = Ligand.from_file(file_name=get_data(file_name)) assert mol.n_atoms > 1 assert mol.n_bonds > 1 assert mol.name is not None
def test_add_conformers(file_name): """ Load up the bace pdb and then add conformers to it from other file types. """ mol = Ligand.from_file(file_name=get_data("bace0.pdb")) mol.coordinates = None mol.add_conformer(file_name=get_data(file_name)) assert mol.coordinates.shape == (mol.n_atoms, 3)
def test_make_unique_names(): """ After loading a molecule with non unique atom names make sure a unique set is automatically generated. """ # load the molecule with missing names mol = Ligand.from_file(get_data("missing_names.pdb")) # make sure they have been converted assert mol.has_unique_atom_names is True
def test_to_mapped_smiles(): """ Make sure the the mapped smiles flag is respected. """ mol = Ligand.from_file(file_name=get_data("bace0.sdf")) no_map = mol.to_smiles(isomeric=True, explicit_hydrogens=True, mapped=False) mapped = mol.to_smiles(isomeric=True, explicit_hydrogens=True, mapped=True) assert no_map != mapped
def test_rotatable_bonds(): """ Make sure we can find true rotatable bonds for a molecule. """ mol = Ligand.from_file(file_name=get_data("biphenyl.pdb")) assert mol.rotatable_bonds == [ (3, 4), ] assert mol.n_rotatable_bonds == 1
def test_parametrise_none(tmpdir): """ If no engine is passed make sure we init the parameter holders but store nothing. """ with tmpdir.as_cwd(): mol = Ligand.from_file(get_data("acetone.pdb")) mol.parameter_engine = "none" param_mol = Execute.parametrise(molecule=mol, verbose=False) for i in range(param_mol.n_atoms): assert param_mol.NonbondedForce[i] == [0, 0, 0]
def test_parametrise_missing_file(tmpdir): """ If a missing file is provided make sure an error is raised. """ with tmpdir.as_cwd(): mol = Ligand.from_file(get_data("acetone.pdb")) mol.home = os.getcwd() mol.parameter_engine = "xml" with pytest.raises(FileNotFoundError): _ = Execute.parametrise(molecule=mol, verbose=False)
def test_to_topology(molecule): """ Make sure that a topology generated using qubekit matches an openff one. """ mol = Ligand.from_file(file_name=get_data(molecule)) offmol = OFFMolecule.from_file(file_path=get_data(molecule)) assert ( nx.algorithms.isomorphism.is_isomorphic(mol.to_topology(), offmol.to_networkx()) is True )
def test_to_smiles_isomeric(): """ Make sure we can write out smiles strings with the correct settings. """ # use bace as it has a chiral center mol = Ligand.from_file(file_name=get_data("bace0.sdf")) smiles = mol.to_smiles(isomeric=True, explicit_hydrogens=False, mapped=False) assert "@@" in smiles smiles = mol.to_smiles(isomeric=False, explicit_hydrogens=False, mapped=False) assert "@" not in smiles
def test_xml_with_sites(tmpdir): """ Make sure that virtual sites are saved into the ligand if they are found in the input file. """ with tmpdir.as_cwd(): mol = Ligand.from_file(get_data("pyridine.pdb")) XML(mol, input_file=get_data("pyridine.xml")) assert mol.extra_sites is not None assert len(mol.extra_sites) == 1 # make sure that charge we extracted assert mol.extra_sites[0].charge == -0.180000
def test_openff_skeleton(tmpdir): """ Make sure the skeleton method in openff works when we have missing coverage in the openff forcefield. This will add generic parameters to the forcefield which should match any missing terms, no charges are generated this way. """ with tmpdir.as_cwd(): # load a molecule with b mol = Ligand.from_file(get_data("132-Benzodioxaborole.pdb")) OpenFF(mol) # no charges should be generated for i in range(mol.n_atoms): assert mol.NonbondedForce[i][0] == 0
def test_parametrise_all(parameter_engine, tmpdir): """ For each parameter engine make sure the molecule is correctly parameterised. """ with tmpdir.as_cwd(): mol = Ligand.from_file(get_data("pyridine.sdf")) mol.parameter_engine = parameter_engine if parameter_engine == "xml": shutil.copy(get_data("pyridine.xml"), "pyridine.xml") param_mol = Execute.parametrise(molecule=mol, verbose=False) # make sure parameters have been found for i in range(param_mol.n_atoms): assert param_mol.NonbondedForce[i] != [0, 0, 0]
def test_write_xyz_multiple_conformer(tmpdir): """ Make sure we can write multiple conformer xyz files for a molecule. """ with tmpdir.as_cwd(): mol = Ligand.from_file(file_name=get_data("butane.pdb")) # fake a set of conformers coords = [mol.coordinates, np.random.random((mol.n_atoms, 3))] mol.to_multiconformer_file(file_name="butane.xyz", positions=coords) # now read in the file again with open("butane.xyz") as xyz: lines = xyz.readlines() assert len(lines) == 2 * mol.n_atoms + 4
def test_parameter_prep(): """Test that the base parameter class preps a molecule to store prameters.""" mol = Ligand.from_file(get_data("acetone.sdf")) assert mol.AtomTypes is None assert mol.HarmonicBondForce is None assert mol.HarmonicAngleForce is None assert mol.PeriodicTorsionForce is None assert mol.NonbondedForce is None # now use the base class to prep the molecule Parametrisation(mol) assert mol.AtomTypes == {} assert len(mol.HarmonicBondForce) == mol.n_bonds assert len(mol.HarmonicAngleForce) == mol.n_angles assert len(mol.NonbondedForce) == mol.n_atoms
def test_sdf_round_trip(tmpdir, acetone): """ Make sure we can write a molecule to pdb and load it back. """ with tmpdir.as_cwd(): acetone.to_file(file_name="test.sdf") mol2 = Ligand.from_file(file_name="test.sdf") for atom in acetone.atoms: pickle_atom = mol2.get_atom_with_name(atom.atom_name) assert pickle_atom.dict() == atom.dict() for i in range(acetone.n_bonds): assert acetone.bonds[i].dict() == mol2.bonds[i].dict() assert acetone.angles == mol2.angles assert acetone.dihedrals == mol2.dihedrals
def test_parameter_engines(tmpdir, parameter_engine): """ Make sure we can parametrise a molecule using antechamber """ with tmpdir.as_cwd(): mol = Ligand.from_file(get_data("acetone.sdf")) parameter_engine(mol) # loop over the parameters and make sure they not defaults for bond in mol.bonds: assert mol.HarmonicBondForce[(bond.atom1_index, bond.atom2_index)] != [0, 0] for angle in mol.angles: assert mol.HarmonicAngleForce[angle] != [0, 0] assert ( len(mol.PeriodicTorsionForce) == mol.n_dihedrals + mol.n_improper_torsions ) for i in range(mol.n_atoms): assert mol.NonbondedForce[i] != [0, 0, 0]
def test_mod_sem_no_symmetry(tmpdir): """ A simple regression test for the modified seminario method. # TODO expand these tests """ with tmpdir.as_cwd(): # the sdf file is at the qm geometry mol = Ligand.from_file(file_name=get_data("ethane.sdf")) hessian = np.loadtxt(fname=get_data("ethane_hessian.txt")) mol.hessian = hessian mod_sem = ModSeminario(molecule=mol) mod_sem.modified_seminario_method() # now check the values assert mol.HarmonicBondForce[( 0, 1)][0] == pytest.approx(0.15344171531887932) assert mol.HarmonicBondForce[( 0, 1)][1] == pytest.approx(192462.76956156612) assert mol.HarmonicBondForce[( 0, 2)][0] == pytest.approx(0.10954907576059233) assert mol.HarmonicBondForce[( 0, 2)][1] == pytest.approx(295645.6124892813) assert mol.HarmonicAngleForce[( 1, 0, 2)][0] == pytest.approx(1.9423960113296368) assert mol.HarmonicAngleForce[( 1, 0, 2)][1] == pytest.approx(374.76469990519263) assert mol.HarmonicAngleForce[( 1, 0, 3)][0] == pytest.approx(1.9422108316309619) assert mol.HarmonicAngleForce[( 1, 0, 3)][1] == pytest.approx(401.56353614024914) assert mol.HarmonicAngleForce[( 1, 0, 4)][0] == pytest.approx(1.9416241741805782) assert mol.HarmonicAngleForce[( 1, 0, 4)][1] == pytest.approx(371.0717571027322) assert mol.HarmonicAngleForce[( 2, 0, 3)][0] == pytest.approx(1.8787818480998344) assert mol.HarmonicAngleForce[( 2, 0, 3)][1] == pytest.approx(314.5677633711689) assert mol.HarmonicAngleForce[( 0, 1, 6)][0] == pytest.approx(1.9423960113296368) assert mol.HarmonicAngleForce[( 0, 1, 6)][1] == pytest.approx(399.59297576081184)
def test_from_rdkit(): """ Make sure we can create a molecule directly from an rdkit object. """ # load a molecule with openff offmol = OFFMolecule.from_file(file_path=get_data("bace0.sdf")) # make a ligand from the openff object mol = Ligand.from_rdkit(rdkit_mol=offmol.to_rdkit()) # make sure we have the same molecule mol2 = Ligand.from_file(get_data("bace0.sdf")) for i in range(mol.n_atoms): atom1 = mol.atoms[i] atom2 = mol2.atoms[i] assert atom1.dict() == atom2.dict() for i in range(mol.n_bonds): bond1 = mol.bonds[i] bon2 = mol2.bonds[i] assert bond1.dict() == bon2.dict() assert np.allclose(mol.coordinates, mol2.coordinates)
def test_mod_sem_symmetry(tmpdir): """ A simple regression test for modified seminario method with symmetry. """ with tmpdir.as_cwd(): # the sdf file is at the qm geometry mol = Ligand.from_file(file_name=get_data("ethane.sdf")) hessian = np.loadtxt(fname=get_data("ethane_hessian.txt")) mol.hessian = hessian mod_sem = ModSeminario(molecule=mol) mod_sem.modified_seminario_method() mod_sem.symmetrise_bonded_parameters() # make sure symmetry groups are the same for bonds in mol.bond_types.values(): values = set() for bond in bonds: values.add(tuple(mol.HarmonicBondForce[bond])) assert len(values) == 1 for angles in mol.angle_types.values(): values = set() for angle in angles: values.add(tuple(mol.HarmonicAngleForce[angle])) assert len(values) == 1
def test_ligand_file_missing(): """ Make sure that if the file is missing we get an error. """ with pytest.raises(FileNotFoundError): _ = Ligand.from_file(file_name="test.pdb")
def test_str(trunc): """ Make sure that the ligand str method does not raise an error. """ mol = Ligand.from_file(file_name=get_data("water.pdb")) mol.__str__(trunc=trunc)
def test_charge(molecule, charge): """ Make sure that the charge is correctly identified. """ mol = Ligand.from_file(file_name=get_data(molecule)) assert mol.charge == charge
def test_ligand_file_not_supported(): """ Make sure we raise an error when an unsupported file type is passed. """ with pytest.raises(FileTypeError): _ = Ligand.from_file(file_name=get_data("bace0.xyz"))
def mol(): """ Initialise the Ligand molecule object with data for Chloromethane """ molecule = Ligand.from_file(file_name=get_data("chloromethane.pdb")) molecule.ddec_data = { 0: CustomNamespace( a_i=72461.2438863321, atomic_symbol="C", b_i=36.09781017184126, charge=-0.220088, r_aim=1.9933297947778903, volume=30.276517, ), 1: CustomNamespace( a_i=153692.84134145387, atomic_symbol="Cl", b_i=101.44341268889193, charge=1.815899, r_aim=1.9020122149415648, volume=67.413573, ), 2: CustomNamespace( a_i=149.1117208173859, atomic_symbol="H", b_i=1.247688109065071, charge=0.13473, r_aim=1.2455924332095252, volume=3.329737, ), 3: CustomNamespace( a_i=149.1117208173859, atomic_symbol="H", b_i=1.247688109065071, charge=0.13473, r_aim=1.2455924332095252, volume=3.329737, ), 4: CustomNamespace( a_i=149.1117208173859, atomic_symbol="H", b_i=1.247688109065071, charge=0.134729, r_aim=1.2455924332095252, volume=3.329737, ), } molecule.dipole_moment_data = { 0: CustomNamespace(x_dipole=0.109154, y_dipole=0.006347, z_dipole=-0.000885), 1: CustomNamespace(x_dipole=-0.139599, y_dipole=-0.006372, z_dipole=0.000994), 2: CustomNamespace(x_dipole=-0.005778, y_dipole=-0.018142, z_dipole=-0.029462), 3: CustomNamespace(x_dipole=-0.00516, y_dipole=-0.016898, z_dipole=0.030335), 4: CustomNamespace(x_dipole=-0.00839, y_dipole=0.035106, z_dipole=-0.000628), } molecule.quadrupole_moment_data = { 0: CustomNamespace( q_3z2_r2=-0.150042, q_x2_y2=0.148149, q_xy=0.007494, q_xz=-0.001301, q_yz=-0.000128, ), 1: CustomNamespace( q_3z2_r2=-1.074695, q_x2_y2=1.070914, q_xy=0.052325, q_xz=-0.006765, q_yz=-0.000286, ), 2: CustomNamespace( q_3z2_r2=0.013971, q_x2_y2=0.011282, q_xy=0.001128, q_xz=0.000274, q_yz=0.011593, ), 3: CustomNamespace( q_3z2_r2=0.01544, q_x2_y2=0.011683, q_xy=0.001125, q_xz=-0.000412, q_yz=-0.01131, ), 4: CustomNamespace( q_3z2_r2=-0.043386, q_x2_y2=-0.007519, q_xy=-0.001058, q_xz=-5.4e-05, q_yz=-0.000249, ), } molecule.cloud_pen_data = { 0: CustomNamespace(a=2.102843, atomic_symbol="C", b=2.40575), 1: CustomNamespace(a=7.939831, atomic_symbol="Cl", b=3.395079), 2: CustomNamespace(a=0.1242, atomic_symbol="H", b=2.533532), 3: CustomNamespace(a=0.123448, atomic_symbol="H", b=2.533309), 4: CustomNamespace(a=0.120282, atomic_symbol="H", b=2.533191), } print(f"mol coords: {molecule.coordinates}") return molecule