From bc82d81c45abc9e2a9759e0cbac2c68ca6325359 Mon Sep 17 00:00:00 2001 From: Greg Landrum Date: Fri, 3 Jul 2020 19:17:54 +0200 Subject: [PATCH] Fixes #2757 (#3268) * Fixes #2757 Note that fix here really means changing the way the doctest uses the code * add py38 to build matrix * adjust to more modern boost * update another set of expected results * re-enable the other CI configs --- .azure-pipelines/linux_build.yml | 5 +- .azure-pipelines/linux_build_py37.yml | 74 +++++++ azure-pipelines.yml | 16 +- rdkit/Chem/EnumerateStereoisomers.py | 289 +++++++++++++------------- rdkit/Chem/UnitTestMol3D.py | 156 ++++++++------ 5 files changed, 330 insertions(+), 210 deletions(-) create mode 100644 .azure-pipelines/linux_build_py37.yml diff --git a/.azure-pipelines/linux_build.yml b/.azure-pipelines/linux_build.yml index 8777b51d4..2dc092e71 100644 --- a/.azure-pipelines/linux_build.yml +++ b/.azure-pipelines/linux_build.yml @@ -5,9 +5,9 @@ steps: conda config --set always_yes yes --set changeps1 no conda update -q conda conda info -a - conda create --name rdkit_build $(compiler) cmake \ + conda create --name rdkit_build $(python) $(compiler) cmake \ boost-cpp=$(boost_version) boost=$(boost_version) \ - py-boost=$(boost_version) libboost=$(boost_version) \ + py-boost=$(boost_version) \ numpy matplotlib pillow eigen pandas \ sphinx recommonmark jupyter conda activate rdkit_build @@ -36,6 +36,7 @@ steps: -DRDK_BUILD_THREADSAFE_SSS=ON \ -DRDK_TEST_MULTITHREADED=ON \ -DBoost_NO_SYSTEM_PATHS=ON \ + -DBoost_NO_BOOST_CMAKE=TRUE \ -DRDK_BOOST_PYTHON3_NAME=$(python_name) \ -DPYTHON_EXECUTABLE=${CONDA_PREFIX}/bin/python3 \ -DCMAKE_INCLUDE_PATH="${CONDA_PREFIX}/include" \ diff --git a/.azure-pipelines/linux_build_py37.yml b/.azure-pipelines/linux_build_py37.yml new file mode 100644 index 000000000..8777b51d4 --- /dev/null +++ b/.azure-pipelines/linux_build_py37.yml @@ -0,0 +1,74 @@ +steps: +- bash: | + source ${CONDA}/etc/profile.d/conda.sh + sudo chown -R ${USER} ${CONDA} + conda config --set always_yes yes --set changeps1 no + conda update -q conda + conda info -a + conda create --name rdkit_build $(compiler) cmake \ + boost-cpp=$(boost_version) boost=$(boost_version) \ + py-boost=$(boost_version) libboost=$(boost_version) \ + numpy matplotlib pillow eigen pandas \ + sphinx recommonmark jupyter + conda activate rdkit_build + conda install -c rdkit nox cairo=1.14.6 + displayName: Setup build environment +- bash: | + source ${CONDA}/etc/profile.d/conda.sh + conda activate rdkit_build + mkdir build && cd build && \ + cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DRDK_INSTALL_INTREE=ON \ + -DRDK_INSTALL_STATIC_LIBS=OFF \ + -DRDK_BUILD_CPP_TESTS=ON \ + -DRDK_BUILD_PYTHON_WRAPPERS=ON \ + -DRDK_USE_BOOST_REGEX=ON \ + -DRDK_BUILD_COORDGEN_SUPPORT=ON \ + -DRDK_OPTIMIZE_POPCNT=ON \ + -DRDK_BUILD_TEST_GZIP=ON \ + -DRDK_BUILD_FREESASA_SUPPORT=ON \ + -DRDK_BUILD_AVALON_SUPPORT=ON \ + -DRDK_BUILD_INCHI_SUPPORT=ON \ + -DRDK_BUILD_CAIRO_SUPPORT=ON \ + -DRDK_BUILD_SWIG_WRAPPERS=OFF \ + -DRDK_SWIG_STATIC=OFF \ + -DRDK_BUILD_THREADSAFE_SSS=ON \ + -DRDK_TEST_MULTITHREADED=ON \ + -DBoost_NO_SYSTEM_PATHS=ON \ + -DRDK_BOOST_PYTHON3_NAME=$(python_name) \ + -DPYTHON_EXECUTABLE=${CONDA_PREFIX}/bin/python3 \ + -DCMAKE_INCLUDE_PATH="${CONDA_PREFIX}/include" \ + -DCMAKE_LIBRARY_PATH="${CONDA_PREFIX}/lib" + displayName: Configure build (Run CMake) +- bash: | + source ${CONDA}/etc/profile.d/conda.sh + conda activate rdkit_build + cd build + make -j $( $(number_of_cores) ) install + displayName: Build +- bash: | + source ${CONDA}/etc/profile.d/conda.sh + conda activate rdkit_build + export RDBASE=`pwd` + export PYTHONPATH=${RDBASE}:${PYTHONPATH} + export LD_LIBRARY_PATH=${RDBASE}/lib:${LD_LIBRARY_PATH} + export QT_QPA_PLATFORM='offscreen' + cd build + ctest -j $( $(number_of_cores) ) --output-on-failure -T Test + displayName: Run tests +- bash: | + source ${CONDA}/etc/profile.d/conda.sh + conda activate rdkit_build + export RDBASE=`pwd` + export PYTHONPATH=${RDBASE}:${PYTHONPATH} + export LD_LIBRARY_PATH=${RDBASE}/lib:${LD_LIBRARY_PATH} + export QT_QPA_PLATFORM='offscreen' + cd Docs/Book + make doctest + displayName: Run documentation tests +- task: PublishTestResults@2 + inputs: + testResultsFormat: 'CTest' + testResultsFiles: 'build/Testing/*/Test.xml' + testRunTitle: $(system.phasedisplayname) CTest Test Run diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3e199cb81..55b14a627 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,7 +4,19 @@ trigger: - dev/* jobs: -- job: Ubuntu_16_04_x64 +- job: Ubuntu_18_04_x64 + timeoutInMinutes: 90 + pool: + vmImage: ubuntu-18.04 + variables: + python: python=3.8 + boost_version: 1.71.0 + compiler: gxx_linux-64 + number_of_cores: nproc + python_name: python38 + steps: + - template: .azure-pipelines/linux_build.yml +- job: Ubuntu_16_04_x64_py37 timeoutInMinutes: 90 pool: vmImage: ubuntu-16.04 @@ -14,7 +26,7 @@ jobs: number_of_cores: nproc python_name: python37 steps: - - template: .azure-pipelines/linux_build.yml + - template: .azure-pipelines/linux_build_py37.yml - job: macOS_10_13_x64 timeoutInMinutes: 90 pool: diff --git a/rdkit/Chem/EnumerateStereoisomers.py b/rdkit/Chem/EnumerateStereoisomers.py index 3c62ca3fd..0de68ae82 100644 --- a/rdkit/Chem/EnumerateStereoisomers.py +++ b/rdkit/Chem/EnumerateStereoisomers.py @@ -4,7 +4,7 @@ from rdkit.Chem.rdDistGeom import EmbedMolecule class StereoEnumerationOptions(object): - """ + """ - tryEmbedding: if set the process attempts to generate a standard RDKit distance geometry conformation for the stereisomer. If this fails, we assume that the stereoisomer is non-physical and don't return it. NOTE that this is computationally expensive and is @@ -24,116 +24,115 @@ class StereoEnumerationOptions(object): - onlyStereoGroups: Only find stereoisomers that differ at the StereoGroups associated with the molecule. """ - __slots__ = ('tryEmbedding', 'onlyUnassigned', - 'onlyStereoGroups', 'maxIsomers', 'rand', 'unique') + __slots__ = ('tryEmbedding', 'onlyUnassigned', + 'onlyStereoGroups', 'maxIsomers', 'rand', 'unique') - def __init__(self, tryEmbedding=False, onlyUnassigned=True, - maxIsomers=1024, rand=None, unique=True, - onlyStereoGroups=False): - self.tryEmbedding = tryEmbedding - self.onlyUnassigned = onlyUnassigned - self.onlyStereoGroups = onlyStereoGroups - self.maxIsomers = maxIsomers - self.rand = rand - self.unique = unique + def __init__(self, tryEmbedding=False, onlyUnassigned=True, + maxIsomers=1024, rand=None, unique=True, + onlyStereoGroups=False): + self.tryEmbedding = tryEmbedding + self.onlyUnassigned = onlyUnassigned + self.onlyStereoGroups = onlyStereoGroups + self.maxIsomers = maxIsomers + self.rand = rand + self.unique = unique class _BondFlipper(object): - def __init__(self, bond): - self.bond = bond + def __init__(self, bond): + self.bond = bond - def flip(self, flag): - if flag: - self.bond.SetStereo(Chem.BondStereo.STEREOCIS) - else: - self.bond.SetStereo(Chem.BondStereo.STEREOTRANS) + def flip(self, flag): + if flag: + self.bond.SetStereo(Chem.BondStereo.STEREOCIS) + else: + self.bond.SetStereo(Chem.BondStereo.STEREOTRANS) class _AtomFlipper(object): - def __init__(self, atom): - self.atom = atom + def __init__(self, atom): + self.atom = atom - def flip(self, flag): - if flag: - self.atom.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CW) - else: - self.atom.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CCW) + def flip(self, flag): + if flag: + self.atom.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CW) + else: + self.atom.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CCW) class _StereoGroupFlipper(object): - def __init__(self, group): - self._original_parities = [(a, a.GetChiralTag()) for a in group.GetAtoms()] + def __init__(self, group): + self._original_parities = [(a, a.GetChiralTag()) for a in group.GetAtoms()] - def flip(self, flag): - if flag: - for a, original_parity in self._original_parities: - a.SetChiralTag(original_parity) - else: - for a, original_parity in self._original_parities: - if original_parity == Chem.ChiralType.CHI_TETRAHEDRAL_CW: - a.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CCW) - elif original_parity == Chem.ChiralType.CHI_TETRAHEDRAL_CCW: - a.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CW) + def flip(self, flag): + if flag: + for a, original_parity in self._original_parities: + a.SetChiralTag(original_parity) + else: + for a, original_parity in self._original_parities: + if original_parity == Chem.ChiralType.CHI_TETRAHEDRAL_CW: + a.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CCW) + elif original_parity == Chem.ChiralType.CHI_TETRAHEDRAL_CCW: + a.SetChiralTag(Chem.ChiralType.CHI_TETRAHEDRAL_CW) def _getFlippers(mol, options): - Chem.FindPotentialStereoBonds(mol) + Chem.FindPotentialStereoBonds(mol) - flippers = [] - if not options.onlyStereoGroups: - for atom in mol.GetAtoms(): - if atom.HasProp("_ChiralityPossible"): - if (not options.onlyUnassigned - or atom.GetChiralTag() == Chem.ChiralType.CHI_UNSPECIFIED): - flippers.append(_AtomFlipper(atom)) + flippers = [] + if not options.onlyStereoGroups: + for atom in mol.GetAtoms(): + if atom.HasProp("_ChiralityPossible"): + if (not options.onlyUnassigned + or atom.GetChiralTag() == Chem.ChiralType.CHI_UNSPECIFIED): + flippers.append(_AtomFlipper(atom)) - for bond in mol.GetBonds(): - bstereo = bond.GetStereo() - if bstereo != Chem.BondStereo.STEREONONE: - if (not options.onlyUnassigned - or bstereo == Chem.BondStereo.STEREOANY): - flippers.append(_BondFlipper(bond)) + for bond in mol.GetBonds(): + bstereo = bond.GetStereo() + if bstereo != Chem.BondStereo.STEREONONE: + if (not options.onlyUnassigned + or bstereo == Chem.BondStereo.STEREOANY): + flippers.append(_BondFlipper(bond)) - if options.onlyUnassigned: - # otherwise these will be counted twice - for group in mol.GetStereoGroups(): - if group.GetGroupType() != Chem.StereoGroupType.STEREO_ABSOLUTE: - flippers.append(_StereoGroupFlipper(group)) + if options.onlyUnassigned: + # otherwise these will be counted twice + for group in mol.GetStereoGroups(): + if group.GetGroupType() != Chem.StereoGroupType.STEREO_ABSOLUTE: + flippers.append(_StereoGroupFlipper(group)) - return flippers + return flippers class _RangeBitsGenerator(object): - def __init__(self, nCenters): - self.nCenters = nCenters - - def __iter__(self): - for val in range(2**self.nCenters): - yield val + def __init__(self, nCenters): + self.nCenters = nCenters + def __iter__(self): + for val in range(2**self.nCenters): + yield val class _UniqueRandomBitsGenerator(object): - def __init__(self, nCenters, maxIsomers, rand): - self.nCenters = nCenters - self.maxIsomers = maxIsomers - self.rand = rand - self.already_seen = set() + def __init__(self, nCenters, maxIsomers, rand): + self.nCenters = nCenters + self.maxIsomers = maxIsomers + self.rand = rand + self.already_seen = set() - def __iter__(self): - # note: important that this is not 'while True' otherwise it - # would be possible to have an infinite loop caused by all - # isomers failing the embedding process - while len(self.already_seen) < 2**self.nCenters: - bits = self.rand.getrandbits(self.nCenters) - if bits in self.already_seen: - continue + def __iter__(self): + # note: important that this is not 'while True' otherwise it + # would be possible to have an infinite loop caused by all + # isomers failing the embedding process + while len(self.already_seen) < 2**self.nCenters: + bits = self.rand.getrandbits(self.nCenters) + if bits in self.already_seen: + continue - self.already_seen.add(bits) - yield bits + self.already_seen.add(bits) + yield bits def GetStereoisomerCount(m, options=StereoEnumerationOptions()): - """ returns an estimate (upper bound) of the number of possible stereoisomers for a molecule + """ returns an estimate (upper bound) of the number of possible stereoisomers for a molecule Arguments: - m: the molecule to work with @@ -156,13 +155,12 @@ def GetStereoisomerCount(m, options=StereoEnumerationOptions()): 8 """ - tm = Chem.Mol(m) - flippers = _getFlippers(tm, options) - return 2**len(flippers) - + tm = Chem.Mol(m) + flippers = _getFlippers(tm, options) + return 2**len(flippers) def EnumerateStereoisomers(m, options=StereoEnumerationOptions(), verbose=False): - """ returns a generator that yields possible stereoisomers for a molecule + """ returns a generator that yields possible stereoisomers for a molecule Arguments: - m: the molecule to work with @@ -278,75 +276,74 @@ def EnumerateStereoisomers(m, options=StereoEnumerationOptions(), verbose=False) F[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@H](Cl)Br F[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)Br - Or randomly sample a small subset: + Or randomly sample a small subset. Note that if we want that sampling to be consistent + across python versions we need to provide a random number seed: >>> m = Chem.MolFromSmiles('Br' + '[CH](Cl)' * 20 + 'F') - >>> opts = StereoEnumerationOptions(maxIsomers=3) + >>> opts = StereoEnumerationOptions(maxIsomers=3,rand=0xf00d) >>> isomers = EnumerateStereoisomers(m, options=opts) - >>> for smi in sorted(Chem.MolToSmiles(x, isomericSmiles=True) for x in isomers): - ... print(smi) - F[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)Br - F[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)Br - F[C@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)Br + >>> for smi in isomers: #sorted(Chem.MolToSmiles(x, isomericSmiles=True) for x in isomers): + ... print(Chem.MolToSmiles(smi)) + F[C@@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)Br + F[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)Br + F[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)[C@@H](Cl)[C@H](Cl)[C@@H](Cl)Br + """ + tm = Chem.Mol(m) + for atom in tm.GetAtoms(): + atom.ClearProp("_CIPCode") + # FIX: deal with bonds here too + flippers = _getFlippers(tm, options) + nCenters = len(flippers) + if not nCenters: + yield tm + return - tm = Chem.Mol(m) - for atom in tm.GetAtoms(): - atom.ClearProp("_CIPCode") - # FIX: deal with bonds here too - flippers = _getFlippers(tm, options) - nCenters = len(flippers) - if not nCenters: - yield tm - return - - if (options.maxIsomers == 0 or 2**nCenters <= options.maxIsomers): - bitsource = _RangeBitsGenerator(nCenters) + if (options.maxIsomers == 0 or 2**nCenters <= options.maxIsomers): + bitsource = _RangeBitsGenerator(nCenters) + else: + if options.rand is None: + # deterministic random seed invariant to input atom order + seed = hash(tuple(sorted([(a.GetDegree(), a.GetAtomicNum()) for a in tm.GetAtoms()]))) + rand = random.Random(seed) + elif isinstance(options.rand, random.Random): + # other implementations of Python random number generators + # can inherit from this class to pick up utility methods + rand = options.rand else: - if options.rand is None: - # deterministic random seed invariant to input atom order - seed = hash(tuple(sorted([(a.GetDegree(), a.GetAtomicNum()) for a in tm.GetAtoms()]))) - rand = random.Random(seed) - elif isinstance(options.rand, random.Random): - # other implementations of Python random number generators - # can inherit from this class to pick up utility methods - rand = options.rand - else: - rand = random.Random(options.rand) + rand = random.Random(options.rand) - bitsource = _UniqueRandomBitsGenerator(nCenters, options.maxIsomers, rand) + bitsource = _UniqueRandomBitsGenerator(nCenters, options.maxIsomers, rand) - isomersSeen = set() - numIsomers = 0 - for bitflag in bitsource: - for i in range(nCenters): - flag = bool(bitflag & (1 << i)) - flippers[i].flip(flag) + isomersSeen = set() + numIsomers = 0 + for bitflag in bitsource: + for i in range(nCenters): + flag = bool(bitflag & (1 << i)) + flippers[i].flip(flag) - isomer = Chem.Mol(tm) - if options.unique: - cansmi = Chem.MolToSmiles(isomer, isomericSmiles=True) - if cansmi in isomersSeen: - continue - - isomersSeen.add(cansmi) - - if options.tryEmbedding: - ntm = Chem.AddHs(isomer) - cid = EmbedMolecule(ntm, randomSeed=bitflag) - if cid >= 0: - conf = Chem.Conformer(isomer.GetNumAtoms()) - for aid in range(isomer.GetNumAtoms()): - conf.SetAtomPosition(aid, ntm.GetConformer().GetAtomPosition(aid)) - isomer.AddConformer(conf) - else: - cid = 1 - if cid >= 0: - yield isomer - numIsomers += 1 - if options.maxIsomers != 0 and numIsomers >= options.maxIsomers: - break - elif verbose: - print("%s failed to embed" % (Chem.MolToSmiles(isomer, isomericSmiles=True))) + isomer = Chem.Mol(tm) + if options.unique: + cansmi = Chem.MolToSmiles(isomer, isomericSmiles=True) + if cansmi in isomersSeen: + continue + isomersSeen.add(cansmi) + if options.tryEmbedding: + ntm = Chem.AddHs(isomer) + cid = EmbedMolecule(ntm, randomSeed=bitflag) + if cid >= 0: + conf = Chem.Conformer(isomer.GetNumAtoms()) + for aid in range(isomer.GetNumAtoms()): + conf.SetAtomPosition(aid, ntm.GetConformer().GetAtomPosition(aid)) + isomer.AddConformer(conf) + else: + cid = 1 + if cid >= 0: + yield isomer + numIsomers += 1 + if options.maxIsomers != 0 and numIsomers >= options.maxIsomers: + break + elif verbose: + print("%s failed to embed" % (Chem.MolToSmiles(isomer, isomericSmiles=True))) diff --git a/rdkit/Chem/UnitTestMol3D.py b/rdkit/Chem/UnitTestMol3D.py index e007c0467..fc4b59148 100755 --- a/rdkit/Chem/UnitTestMol3D.py +++ b/rdkit/Chem/UnitTestMol3D.py @@ -3,6 +3,7 @@ """ from rdkit import RDConfig import unittest, os +import sys import random from rdkit import Chem from rdkit.Chem import AllChem @@ -126,8 +127,8 @@ class TestCase(unittest.TestCase): def testTorsionFingerprintsColinearBonds(self): # test that single bonds adjacent to triple bonds are ignored mol = Chem.MolFromSmiles('CCC#CCC') - tors_list, tors_list_rings = TorsionFingerprints.CalculateTorsionLists(mol, - ignoreColinearBonds=True) + tors_list, tors_list_rings = TorsionFingerprints.CalculateTorsionLists( + mol, ignoreColinearBonds=True) self.assertEqual(len(tors_list), 0) weights = TorsionFingerprints.CalculateTorsionWeights(mol, ignoreColinearBonds=True) self.assertEqual(len(weights), 0) @@ -142,8 +143,8 @@ class TestCase(unittest.TestCase): # test that single bonds adjacent to terminal triple bonds are always ignored mol = Chem.MolFromSmiles('C#CCC') - tors_list, tors_list_rings = TorsionFingerprints.CalculateTorsionLists(mol, - ignoreColinearBonds=True) + tors_list, tors_list_rings = TorsionFingerprints.CalculateTorsionLists( + mol, ignoreColinearBonds=True) self.assertEqual(len(tors_list), 0) tors_list, tors_list_rings = TorsionFingerprints.CalculateTorsionLists( mol, ignoreColinearBonds=False) @@ -191,30 +192,39 @@ class TestCase(unittest.TestCase): def testEnumerateStereoisomersBasic(self): mol = Chem.MolFromSmiles('CC(F)=CC(Cl)C') - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol)) + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol)) self.assertEqual(len(smiles), 4) def testEnumerateStereoisomersLargeRandomSample(self): # near max number of stereo centers allowed mol = Chem.MolFromSmiles('CC(F)=CC(Cl)C' * 31) - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol)) + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol)) self.assertEqual(len(smiles), 1024) def testEnumerateStereoisomersWithCrazyNumberOfCenters(self): # insanely large numbers of isomers aren't a problem mol = Chem.MolFromSmiles('CC(F)=CC(Cl)C' * 101) opts = AllChem.StereoEnumerationOptions(rand=None, maxIsomers=13) - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) self.assertEqual(len(smiles), 13) def testEnumerateStereoisomersRandomSamplingShouldBeDeterministicAndPortable(self): mol = Chem.MolFromSmiles('CC(F)=CC(Cl)C=C(Br)C(I)N') opts = AllChem.StereoEnumerationOptions(maxIsomers=2) - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) - expected = set([ + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) + # Python 3.8 switch how tuples were hashed, so we get different results there + if sys.version_info < (3, 8): + expected = set([ 'C/C(F)=C/[C@@H](Cl)/C=C(/Br)[C@@H](N)I', 'C/C(F)=C/[C@H](Cl)/C=C(\\Br)[C@H](N)I', - ]) + ]) + else: + expected = set( + ['C/C(F)=C\\[C@H](Cl)/C=C(\\Br)[C@H](N)I', 'C/C(F)=C/[C@@H](Cl)/C=C(\\Br)[C@H](N)I']) self.assertEqual(smiles, expected) def testEnumerateStereoisomersMaxIsomersShouldBeReturnedEvenWithTryEmbedding(self): @@ -225,7 +235,8 @@ class TestCase(unittest.TestCase): isomers.add(Chem.MolToSmiles(x, isomericSmiles=True)) self.assertEqual(len(isomers), 8) - def testEnumerateStereoisomersTryEmbeddingShouldNotInfiniteLoopWhenMaxIsomersIsLargerThanActual(self): + def testEnumerateStereoisomersTryEmbeddingShouldNotInfiniteLoopWhenMaxIsomersIsLargerThanActual( + self): m = Chem.MolFromSmiles('BrC=CC1OC(C2)(F)C2(Cl)C1') opts = AllChem.StereoEnumerationOptions(tryEmbedding=True, maxIsomers=1024) isomers = set() @@ -236,18 +247,25 @@ class TestCase(unittest.TestCase): def testEnumerateStereoisomersRandomSeeding(self): opts = AllChem.StereoEnumerationOptions(rand=None, maxIsomers=3) mol = Chem.MolFromSmiles('CC(F)=CC(Cl)C') - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) - self.assertEqual(smiles, set(['C/C(F)=C/[C@@H](C)Cl', 'C/C(F)=C\\[C@H](C)Cl', 'C/C(F)=C\\[C@@H](C)Cl'])) + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) + self.assertEqual(smiles, + set(['C/C(F)=C/[C@@H](C)Cl', 'C/C(F)=C\\[C@H](C)Cl', 'C/C(F)=C\\[C@@H](C)Cl'])) opts = AllChem.StereoEnumerationOptions(rand=0xDEADBEEF) mol = Chem.MolFromSmiles('c1ccc2c(c1)C(=O)N(C2=O)C3CCC(=O)NC3=O') - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) - self.assertEqual(smiles, set(['O=C1CC[C@@H](N2C(=O)c3ccccc3C2=O)C(=O)N1', 'O=C1CC[C@H](N2C(=O)c3ccccc3C2=O)C(=O)N1'])) + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) + self.assertEqual( + smiles, + set(['O=C1CC[C@@H](N2C(=O)c3ccccc3C2=O)C(=O)N1', 'O=C1CC[C@H](N2C(=O)c3ccccc3C2=O)C(=O)N1'])) class DeterministicRandom(random.Random): + def __init__(self): random.Random.__init__(self) self.count = 0 + def getrandbits(self, n_bits): c = self.count self.count += 1 @@ -256,75 +274,94 @@ class TestCase(unittest.TestCase): rand = DeterministicRandom() opts = AllChem.StereoEnumerationOptions(rand=rand, maxIsomers=3) mol = Chem.MolFromSmiles('CCCC(=C(CCl)C(C)CBr)[C@H](F)C(C)C') - smiles = [Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)] - self.assertEqual(smiles, ['CCC/C(=C(\\CCl)[C@H](C)CBr)[C@H](F)C(C)C', 'CCC/C(=C(\\CCl)[C@@H](C)CBr)[C@H](F)C(C)C', 'CCC/C(=C(/CCl)[C@H](C)CBr)[C@H](F)C(C)C']) + smiles = [ + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts) + ] + self.assertEqual(smiles, [ + 'CCC/C(=C(\\CCl)[C@H](C)CBr)[C@H](F)C(C)C', 'CCC/C(=C(\\CCl)[C@@H](C)CBr)[C@H](F)C(C)C', + 'CCC/C(=C(/CCl)[C@H](C)CBr)[C@H](F)C(C)C' + ]) def testEnumerateStereoisomersOnlyUnassigned(self): # shouldn't enumerate anything fully_assigned = Chem.MolFromSmiles('C/C(F)=C/[C@@H](C)Cl') - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(fully_assigned)) + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) + for i in AllChem.EnumerateStereoisomers(fully_assigned)) self.assertEqual(smiles, set(['C/C(F)=C/[C@@H](C)Cl'])) # should only enumerate the bond stereo partially_assigned = Chem.MolFromSmiles('CC(F)=C[C@@H](C)Cl') - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(partially_assigned)) + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) + for i in AllChem.EnumerateStereoisomers(partially_assigned)) self.assertEqual(smiles, set(['C/C(F)=C/[C@@H](C)Cl', 'C/C(F)=C\\[C@@H](C)Cl'])) # should enumerate everything opts = AllChem.StereoEnumerationOptions(onlyUnassigned=False) - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(fully_assigned, opts)) - self.assertEqual(smiles, set(['C/C(F)=C\\[C@@H](C)Cl', - 'C/C(F)=C\\[C@H](C)Cl', - 'C/C(F)=C/[C@H](C)Cl', - 'C/C(F)=C/[C@@H](C)Cl',])) + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) + for i in AllChem.EnumerateStereoisomers(fully_assigned, opts)) + self.assertEqual( + smiles, + set([ + 'C/C(F)=C\\[C@@H](C)Cl', + 'C/C(F)=C\\[C@H](C)Cl', + 'C/C(F)=C/[C@H](C)Cl', + 'C/C(F)=C/[C@@H](C)Cl', + ])) def testEnumerateStereoisomersOnlyUnique(self): mol = Chem.MolFromSmiles('FC(Cl)C(Cl)F') opts = AllChem.StereoEnumerationOptions(unique=False) - smiles = [Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)] + smiles = [ + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts) + ] self.assertEqual(len(smiles), 4) self.assertEqual(len(set(smiles)), 3) mol = Chem.MolFromSmiles('FC(Cl)C(Cl)F') opts = AllChem.StereoEnumerationOptions(unique=True) - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) - self.assertEqual(smiles, set(['F[C@@H](Cl)[C@@H](F)Cl', - 'F[C@@H](Cl)[C@H](F)Cl', - 'F[C@H](Cl)[C@H](F)Cl'])) - + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) + self.assertEqual( + smiles, set(['F[C@@H](Cl)[C@@H](F)Cl', 'F[C@@H](Cl)[C@H](F)Cl', 'F[C@H](Cl)[C@H](F)Cl'])) mol = Chem.MolFromSmiles('CC=CC=CC') opts = AllChem.StereoEnumerationOptions(unique=False) - smiles = [Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)] + smiles = [ + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts) + ] self.assertEqual(len(smiles), 4) self.assertEqual(len(set(smiles)), 3) mol = Chem.MolFromSmiles('CC=CC=CC') opts = AllChem.StereoEnumerationOptions(unique=True) - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) - self.assertEqual(smiles, set(['C/C=C/C=C/C', - 'C/C=C\\C=C\\C', - 'C/C=C\\C=C/C'])) + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) + self.assertEqual(smiles, set(['C/C=C/C=C/C', 'C/C=C\\C=C\\C', 'C/C=C\\C=C/C'])) mol = Chem.MolFromSmiles('FC(Cl)C=CC=CC(F)Cl') opts = AllChem.StereoEnumerationOptions(unique=False) - smiles = [Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)] + smiles = [ + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts) + ] self.assertEqual(len(smiles), 16) self.assertEqual(len(set(smiles)), 10) mol = Chem.MolFromSmiles('FC(Cl)C=CC=CC(F)Cl') opts = AllChem.StereoEnumerationOptions(unique=True) - smiles = set(Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) - self.assertEqual(smiles, set(['F[C@H](Cl)/C=C\\C=C\\[C@@H](F)Cl', - 'F[C@H](Cl)/C=C/C=C/[C@H](F)Cl', - 'F[C@H](Cl)/C=C/C=C/[C@@H](F)Cl', - 'F[C@H](Cl)/C=C\\C=C/[C@@H](F)Cl', - 'F[C@H](Cl)/C=C\\C=C\\[C@H](F)Cl', - 'F[C@H](Cl)/C=C\\C=C/[C@H](F)Cl', - 'F[C@@H](Cl)/C=C/C=C/[C@@H](F)Cl', - 'F[C@@H](Cl)/C=C\\C=C\\[C@H](F)Cl', - 'F[C@@H](Cl)/C=C\\C=C\\[C@@H](F)Cl', - 'F[C@@H](Cl)/C=C\\C=C/[C@@H](F)Cl'])) + smiles = set( + Chem.MolToSmiles(i, isomericSmiles=True) for i in AllChem.EnumerateStereoisomers(mol, opts)) + self.assertEqual( + smiles, + set([ + 'F[C@H](Cl)/C=C\\C=C\\[C@@H](F)Cl', 'F[C@H](Cl)/C=C/C=C/[C@H](F)Cl', + 'F[C@H](Cl)/C=C/C=C/[C@@H](F)Cl', 'F[C@H](Cl)/C=C\\C=C/[C@@H](F)Cl', + 'F[C@H](Cl)/C=C\\C=C\\[C@H](F)Cl', 'F[C@H](Cl)/C=C\\C=C/[C@H](F)Cl', + 'F[C@@H](Cl)/C=C/C=C/[C@@H](F)Cl', 'F[C@@H](Cl)/C=C\\C=C\\[C@H](F)Cl', + 'F[C@@H](Cl)/C=C\\C=C\\[C@@H](F)Cl', 'F[C@@H](Cl)/C=C\\C=C/[C@@H](F)Cl' + ])) def testEnumerateStereoisomersOnlyEnhancedStereo(self): rdbase = os.environ["RDBASE"] @@ -332,8 +369,7 @@ class TestCase(unittest.TestCase): mol = Chem.MolFromMolFile(filename) smiles = set(Chem.MolToSmiles(m) for m in AllChem.EnumerateStereoisomers(mol)) # switches the centers linked by an "OR", but not the absolute group - self.assertEqual(smiles, {r'C[C@@H](F)[C@@H](C)[C@@H](C)Br', - r'C[C@H](F)[C@H](C)[C@@H](C)Br'}) + self.assertEqual(smiles, {r'C[C@@H](F)[C@@H](C)[C@@H](C)Br', r'C[C@H](F)[C@H](C)[C@@H](C)Br'}) original_smiles = Chem.MolToSmiles(mol) self.assertIn(original_smiles, smiles) @@ -361,24 +397,24 @@ class TestCase(unittest.TestCase): def testIssue3231(self): mol = Chem.MolFromSmiles( 'C[C@H](OC1=C(N)N=CC(C2=CN(C3C[C@H](C)NCC3)N=C2)=C1)C4=C(Cl)C=CC(F)=C4Cl') - Chem.AssignStereochemistry(mol,force=True,flagPossibleStereoCenters=True) - l = Chem.FindMolChiralCenters(mol,includeUnassigned=True) + Chem.AssignStereochemistry(mol, force=True, flagPossibleStereoCenters=True) + l = Chem.FindMolChiralCenters(mol, includeUnassigned=True) self.assertEqual(l, [(1, 'S'), (12, '?'), (14, 'S')]) enumsi_opt = AllChem.StereoEnumerationOptions(maxIsomers=20, onlyUnassigned=False) isomers = list(AllChem.EnumerateStereoisomers(mol, enumsi_opt)) chi_cents = [] for iso in isomers: Chem.AssignStereochemistry(iso) - chi_cents.append(Chem.FindMolChiralCenters(iso,includeUnassigned=True)) + chi_cents.append(Chem.FindMolChiralCenters(iso, includeUnassigned=True)) self.assertEqual(sorted(chi_cents), - [[(1, 'R'), (12, 'R'), (14, 'R')], - [(1, 'R'), (12, 'R'), (14, 'S')], - [(1, 'R'), (12, 'S'), (14, 'R')], - [(1, 'R'), (12, 'S'), (14, 'S')], - [(1, 'S'), (12, 'R'), (14, 'R')], - [(1, 'S'), (12, 'R'), (14, 'S')], - [(1, 'S'), (12, 'S'), (14, 'R')], - [(1, 'S'), (12, 'S'), (14, 'S')]]) + [[(1, 'R'), (12, 'R'), + (14, 'R')], [(1, 'R'), (12, 'R'), + (14, 'S')], [(1, 'R'), (12, 'S'), + (14, 'R')], [(1, 'R'), (12, 'S'), (14, 'S')], + [(1, 'S'), (12, 'R'), + (14, 'R')], [(1, 'S'), (12, 'R'), + (14, 'S')], [(1, 'S'), (12, 'S'), + (14, 'R')], [(1, 'S'), (12, 'S'), (14, 'S')]]) if __name__ == '__main__':