--- a +++ b/tests/test_spatial.py @@ -0,0 +1,187 @@ +import os + +import pytest +from numpy.testing import (assert_almost_equal, + assert_array_equal, + assert_array_almost_equal) +import numpy as np + +import oddt +from oddt.spatial import (angle, + dihedral, + rmsd, + distance, + rotate) +from .utils import shuffle_mol + + +test_data_dir = os.path.dirname(os.path.abspath(__file__)) + +ASPIRIN_SDF = """ + RDKit 3D + + 13 13 0 0 0 0 0 0 0 0999 V2000 + 3.3558 -0.4356 -1.0951 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.0868 -0.6330 -0.3319 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.0284 -0.9314 0.8534 O 0 0 0 0 0 0 0 0 0 0 0 0 + 1.0157 -0.4307 -1.1906 O 0 0 0 0 0 0 0 0 0 0 0 0 + -0.2079 -0.5332 -0.5260 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.9020 -1.7350 -0.6775 C 0 0 0 0 0 0 0 0 0 0 0 0 + -2.1373 -1.8996 -0.0586 C 0 0 0 0 0 0 0 0 0 0 0 0 + -2.6805 -0.8641 0.6975 C 0 0 0 0 0 0 0 0 0 0 0 0 + -1.9933 0.3419 0.8273 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.7523 0.5244 0.2125 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.0600 1.8264 0.3368 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.9397 2.1527 -0.2811 O 0 0 0 0 0 0 0 0 0 0 0 0 + -0.6931 2.6171 1.2333 O 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 + 2 3 2 0 + 2 4 1 0 + 4 5 1 0 + 5 6 2 0 + 6 7 1 0 + 7 8 2 0 + 8 9 1 0 + 9 10 2 0 + 10 11 1 0 + 11 12 2 0 + 11 13 1 0 + 10 5 1 0 +M END + +""" + + +def test_angles(): + """Test spatial computations - angles""" + + # Angles + assert_array_almost_equal(angle(np.array((1, 0, 0)), + np.array((0, 0, 0)), + np.array((0, 1, 0))), 90) + + assert_array_almost_equal(angle(np.array((1, 0, 0)), + np.array((0, 0, 0)), + np.array((1, 1, 0))), 45) + + # Check benzene ring angle + mol = oddt.toolkit.readstring('smi', 'c1ccccc1') + mol.make3D() + assert_array_almost_equal(angle(mol.coords[0], + mol.coords[1], + mol.coords[2]), 120, decimal=1) + + +def test_dihedral(): + """Test dihedrals""" + # Dihedrals + assert_array_almost_equal(dihedral(np.array((1, 0, 0)), + np.array((0, 0, 0)), + np.array((0, 1, 0)), + np.array((1, 1, 0))), 0) + + assert_array_almost_equal(dihedral(np.array((1, 0, 0)), + np.array((0, 0, 0)), + np.array((0, 1, 0)), + np.array((1, 1, 1))), -45) + + # Check benzene ring dihedral + mol = oddt.toolkit.readstring('smi', 'c1ccccc1') + mol.make3D() + assert abs(dihedral(*mol.coords[:4])) < 2. + + +def test_distance(): + mol1 = oddt.toolkit.readstring('sdf', ASPIRIN_SDF) + d = distance(mol1.coords, mol1.coords) + n_atoms = len(mol1.coords) + assert d.shape, (n_atoms == n_atoms) + assert_array_equal(d[np.eye(len(mol1.coords), dtype=bool)], np.zeros(n_atoms)) + + d = distance(mol1.coords, mol1.coords.mean(axis=0).reshape(1, 3)) + assert d.shape, (n_atoms == 1) + ref_dist = [[3.556736951371501], [2.2058040428631056], [2.3896002745745415], + [1.6231668718498249], [0.7772981740050453], [2.0694947503940004], + [2.8600587871157184], [2.9014207091233857], [2.1850791695403564], + [0.9413368403116871], [1.8581710293650173], [2.365629642108773], + [2.975007440512798]] + assert_array_almost_equal(d, ref_dist) + + +def test_spatial(): + """Test spatial misc computations""" + mol = oddt.toolkit.readstring('smi', 'c1ccccc1') + mol.make3D() + mol2 = mol.clone + # Test rotation + assert_almost_equal(mol2.coords, rotate(mol2.coords, np.pi, np.pi, np.pi)) + + # Rotate perpendicular to ring + mol2.coords = rotate(mol2.coords, 0, 0, np.pi) + + # RMSD + assert_almost_equal(rmsd(mol, mol2, method=None), 2.77, decimal=1) + # Hungarian must be close to zero (RDKit is 0.3) + assert_almost_equal(rmsd(mol, mol2, method='hungarian'), 0, decimal=0) + # Minimized by symetry must close to zero + assert_almost_equal(rmsd(mol, mol2, method='min_symmetry'), 0, decimal=0) + + +def test_rmsd(): + # pick one molecule from docked poses + mols = list(oddt.toolkit.readfile('sdf', os.path.join(test_data_dir, 'data/dude/xiap/actives_docked.sdf'))) + mols = list(filter(lambda x: x.title == '312335', mols)) + + res = { + 'method=None': + [4.7536, 2.5015, 2.7942, 1.1282, 0.7444, 1.6257, 4.7625, + 2.7168, 2.5504, 1.9304, 2.6201, 3.1742, 3.2254, 4.7785, + 4.8035, 7.8963, 2.2385, 4.8625, 3.2037], + 'method=hungarian': + [0.9013, 1.0730, 1.0531, 1.0286, 0.7353, 1.4094, 0.5391, + 1.3297, 1.0881, 1.7796, 2.6064, 3.1577, 3.2135, 0.8126, + 1.2909, 2.5217, 2.0836, 1.8325, 3.1874], + 'method=min_symmetry': + [0.9013, 1.0732, 1.0797, 1.0492, 0.7444, 1.6257, 0.5391, + 1.5884, 1.0935, 1.9304, 2.6201, 3.1742, 3.2254, 1.1513, + 1.5206, 2.5361, 2.2385, 1.971, 3.2037], + } + + kwargs_grid = [{'method': None}, + {'method': 'hungarian'}, + {'method': 'min_symmetry'}] + for kwargs in kwargs_grid: + res_key = '_'.join('%s=%s' % (k, v) + for k, v in sorted(kwargs.items())) + assert_array_almost_equal([rmsd(mols[0], mol, **kwargs) + for mol in mols[1:]], + res[res_key], decimal=4) + + # test shuffled rmsd + for _ in range(5): + for kwargs in kwargs_grid: + # dont use method=None in shuffled tests + if kwargs['method'] is None: + continue + res_key = '_'.join('%s=%s' % (k, v) + for k, v in sorted(kwargs.items())) + assert_array_almost_equal([rmsd(mols[0], + shuffle_mol(mol), + **kwargs) + for mol in mols[1:]], + res[res_key], decimal=4) + + +def test_rmsd_errors(): + mol = oddt.toolkit.readstring('smi', 'c1ccccc1') + mol.make3D() + mol.addh() + mol2 = next(oddt.toolkit.readfile('sdf', os.path.join(test_data_dir, 'data/dude/xiap/actives_docked.sdf'))) + + for method in [None, 'hungarian', 'min_symmetry']: + with pytest.raises(ValueError, match='Unequal number of atoms'): + rmsd(mol, mol2, method=method) + + for _ in range(5): + with pytest.raises(ValueError, match='Unequal number of atoms'): + rmsd(shuffle_mol(mol), shuffle_mol(mol2), method=method)