# Copyright (c) 2016 Martin Sicho
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os, sys
import unittest
from io import StringIO, BytesIO
from pkg_resources import resource_filename
from rdkit import Chem
from molpher import random
from molpher.core import ExplorationTree
from molpher.core import MolpherMol
from molpher.core import ExplorationData
from molpher.core import MolpherAtom
from molpher.core.morphing import AtomLibrary
from molpher.core.morphing import Molpher
from molpher.core.morphing.operators import *
from molpher.core.selectors import *
[docs]class TestAPI(unittest.TestCase):
[docs] @staticmethod
def getPathToMol(tree, mol):
assert tree.hasMol(mol)
path = []
current = mol
while current.parent_smiles:
path.append(current)
current = tree.fetchMol(current.parent_smiles)
path.reverse()
return path
[docs] def setUp(self):
random.set_random_seed(42)
self.test_source = 'CCO'
self.test_target = 'C1=COC=C1'
self.test_dir = os.path.abspath(resource_filename('molpher.core.tests', 'test_files/'))
self.test_template_path = os.path.join(self.test_dir, 'test-template.xml')
self.cymene_locked = os.path.join(self.test_dir, 'cymene.sdf')
self.ethanol_locked = os.path.join(self.test_dir, 'ethanol.sdf')
self.propanol = os.path.join(self.test_dir, 'propanol.sdf')
self.remove_bond_test_mol = os.path.join(self.test_dir, 'remove_bond_test_mol.sdf')
self.alanine = os.path.join(self.test_dir, 'alanine.sdf')
self.isopropylphenol = os.path.join(self.test_dir, 'isopropylphenol.sdf')
self.contract_bond_test_mol = os.path.join(self.test_dir, 'contract_bond_test_mol.sdf')
self.reroute_test_mol = os.path.join(self.test_dir, 'reroute_test_mol.sdf')
self.captopril = os.path.join(self.test_dir, 'captopril.sdf')
[docs] def tearDown(self):
pass
[docs] def testMolpherAtom(self):
carbon = MolpherAtom("C")
oxygen = MolpherAtom("O", -1)
self.assertEqual(carbon.formal_charge, 0)
carbon.formal_charge = 1
self.assertEqual(carbon.formal_charge, 1)
self.assertFalse(oxygen.is_locked or carbon.is_locked)
oxygen.locking_mask = MolpherAtom.FULL_LOCK
self.assertTrue(oxygen.is_locked)
self.assertTrue(oxygen.lock_info['FULL_LOCK'])
self.assertTrue(bool(oxygen.locking_mask & MolpherAtom.NO_ADDITION))
self.assertTrue(bool(oxygen.locking_mask & MolpherAtom.KEEP_NEIGHBORS))
self.assertTrue(bool(oxygen.locking_mask & MolpherAtom.NO_MUTATION))
self.assertTrue(oxygen.lock_info['UNLOCKED'] == False)
carbon.locking_mask = MolpherAtom.NO_MUTATION
self.assertTrue(carbon.is_locked)
self.assertFalse(carbon.lock_info['FULL_LOCK'])
self.assertFalse(bool(carbon.locking_mask & MolpherAtom.NO_ADDITION))
self.assertFalse(bool(carbon.locking_mask & MolpherAtom.KEEP_NEIGHBORS))
self.assertTrue(bool(carbon.locking_mask & MolpherAtom.NO_MUTATION))
self.assertTrue(carbon.lock_info['UNLOCKED'] == False)
[docs] def testMolpherMol(self):
mol = MolpherMol(self.test_target)
self.assertTrue(mol.asRDMol())
self.assertTrue(mol.asMolBlock())
mol.smiles = 'CCC'
self.assertEqual(mol.getSMILES(), 'CCC')
copy = mol.copy()
copy.sascore = 0.54
self.assertEqual(0.54, copy.sascore)
tree = ExplorationTree.create(source=mol.smiles, target='CCCNCCC')
tree = ExplorationTree.create(source=mol, target='CCCNCCC')
tree = ExplorationTree.create(source=mol, target=MolpherMol('CCCNCCC'))
self.assertTrue(tree.hasMol(mol))
def assign(x):
tree.fetchMol(mol.smiles).smiles = x
self.assertRaises(RuntimeError, assign, 'CCO')
# atom locking stuff
mol_locked = MolpherMol(self.cymene_locked)
open_positions = (0, 2, 3, 9)
for idx, atom in enumerate(mol_locked.atoms):
if not atom.is_locked:
self.assertIn(idx, open_positions)
else:
self.assertTrue(atom.lock_info['NO_ADDITION'])
self.assertFalse(atom.lock_info['UNLOCKED'])
self.assertFalse(atom.lock_info['FULL_LOCK'])
# test RDKit conversion and locking information transfer
rd_mol = mol_locked.asRDMol()
output = None
if sys.version_info[0] < 3:
output = BytesIO()
else:
output = StringIO()
writer = Chem.SDWriter(output)
writer.write(rd_mol)
writer.close()
temp_path = self.test_dir + "/cymene_tmp.sdf"
with open(temp_path, "w") as tempfile:
tempfile.write(output.getvalue())
new_cymene = MolpherMol(temp_path)
os.remove(temp_path)
for atm_old, atm_new in zip(mol_locked.atoms, new_cymene.atoms):
self.assertTrue(atm_old.locking_mask == atm_new.locking_mask)
# test init from RDKit
mol_from_rdkit = MolpherMol(other=rd_mol)
for atm_old, atm_new in zip(mol_locked.atoms, mol_from_rdkit.atoms):
self.assertTrue(atm_old.locking_mask == atm_new.locking_mask)
[docs] def testAtomLibrary(self):
smbls = ["O", "S"]
my_lib = AtomLibrary(smbls)
for x in my_lib.atoms:
self.assertIn(x.symbol, smbls)
default_lib = AtomLibrary.getDefaultLibrary()
old_atoms = default_lib.atoms
self.assertIn("C", [x.symbol for x in default_lib.atoms])
AtomLibrary.setDefaultLibrary(my_lib)
for x in default_lib.atoms:
self.assertIn(x.symbol, smbls)
for x in range(200):
self.assertIn(default_lib.getRandomAtom().symbol, smbls)
AtomLibrary.setDefaultLibrary(AtomLibrary(old_atoms)) # put everything back to default
[docs] def assertOperatorValid(self, operator, test_mol, gens = 10):
print("Testing operator:", operator)
if not operator.getOriginal():
self.assertRaises(RuntimeError, operator.setOriginal, None)
self.assertRaises(RuntimeError, operator.morph)
mol = MolpherMol(self.propanol)
operator.setOriginal(mol)
orig = operator.getOriginal()
self.assertIsNotNone(orig)
self.assertIsInstance(orig, MolpherMol)
self.assertEqual(orig.getSMILES(), mol.getSMILES())
operator.setOriginal(test_mol)
for x in range(gens):
mol = operator.morph()
if mol:
self.assertIsInstance(mol, MolpherMol)
print(mol.smiles)
operator.setOriginal(mol)
[docs] def testAddAtomOperator(self):
cymene_no_add = MolpherMol(self.cymene_locked)
add_atom = AddAtom()
self.assertOperatorValid(add_atom, cymene_no_add)
[docs] def testAddBondOperator(self):
propanol = MolpherMol(self.propanol)
add_bond = AddBond()
self.assertOperatorValid(add_bond, propanol)
add_bond.setOriginal(propanol)
open_bonds = add_bond.getOpenBonds()
self.assertIsInstance(open_bonds, tuple)
[docs] def testRemoveBondOperator(self):
test_mol = MolpherMol(self.remove_bond_test_mol)
remove_bond = RemoveBond()
self.assertOperatorValid(remove_bond, test_mol)
remove_bond.setOriginal(test_mol)
open_bonds = remove_bond.getOpenBonds()
self.assertIsInstance(open_bonds, tuple)
[docs] def testMutateAtomOperator(self):
self.assertOperatorValid(MutateAtom(), MolpherMol(self.isopropylphenol))
[docs] def testInterlayAtomOperator(self):
self.assertOperatorValid(InterlayAtom(), MolpherMol(self.isopropylphenol))
[docs] def testRerouteBondOperator(self):
self.assertOperatorValid(ContractBond(), MolpherMol(self.contract_bond_test_mol))
[docs] def testContractBondOperator(self):
self.assertOperatorValid(RerouteBond(), MolpherMol(self.reroute_test_mol))
[docs] def testMolpher(self):
cymene = MolpherMol(self.cymene_locked)
operators = [AddAtom(), RemoveAtom()]
molpher = Molpher(cymene, operators)
molpher()
morphs = molpher.getMorphs()
self.assertTrue(morphs)
self.assertFalse(molpher.getMorphs())
morphs = []
while len(morphs) < 50:
morphs.append(molpher.next())
self.assertEqual(50, len(morphs))
[docs] def testMorphingOperator(self):
class Identity(MorphingOperator):
def morph(self):
return self.original.copy()
def getName(self):
return "Identity"
cymene = MolpherMol(self.cymene_locked)
operators = [Identity()]
molpher = Molpher(cymene, operators)
molpher()
for morph in molpher.getMorphs():
self.assertEqual(morph.smiles, cymene.smiles)
[docs] def testExplorationData(self):
params = ExplorationData(
source=self.test_source
, target=self.test_target
)
params.operators = (OP_ADD_BOND, 'OP_REMOVE_BOND',)
self.assertEqual(params.operators, ('OP_ADD_BOND', 'OP_REMOVE_BOND'))
params.fingerprint = FP_EXT_ATOM_PAIRS
self.assertEqual(params.fingerprint, 'FP_EXT_ATOM_PAIRS')
params.fingerprint = 'FP_TOPOLOGICAL_LAYERED_2'
self.assertEqual(params.fingerprint, 'FP_TOPOLOGICAL_LAYERED_2')
params.similarity = 'SC_COSINE'
self.assertEqual(params.similarity, 'SC_COSINE')
params.similarity = SC_KULCZYNSKI
self.assertEqual(params.similarity, 'SC_KULCZYNSKI')
self.assertEqual(params.source.smiles, self.test_source)
self.assertEqual(params.target.smiles, self.test_target)
params.param_dict = {
'target' : self.test_source
, 'operators' : params.param_dict['operators'][:1]
}
self.assertEqual(params.target.smiles, self.test_source)
self.assertEqual(params.operators, ('OP_ADD_BOND',))
params_from_temp = ExplorationData.load(self.test_template_path)
self.assertRaises(RuntimeError, lambda : ExplorationData.load('not/a/valid/path'))
self.assertEqual(777, params_from_temp.far_produce)
[docs] def testTree(self):
mol1 = self.test_source
mol2 = self.test_target
params_dict = {
'source' : mol1
, 'target' : mol2
, 'operators' : (OP_ADD_BOND, OP_REMOVE_BOND, OP_MUTATE_ATOM)
}
params = ExplorationData(**params_dict)
self.assertRaises(AttributeError, lambda : ExplorationTree())
tree_from_dict = ExplorationTree.create(tree_data=params_dict)
tree_from_params = ExplorationTree.create(tree_data=params)
tree_from_SMILES = ExplorationTree.create(source=mol1, target=mol2)
def test_tree(tree):
self.assertEqual(tree.params['source'], mol1)
self.assertEqual(tree.params['target'], mol2)
test_tree(tree_from_dict)
test_tree(tree_from_params)
test_tree(tree_from_SMILES)
tree = tree_from_params
# if we try to set source for non-empty tree, exception should be raised
def func():
tree.params = {
'source' : mol2
, 'target' : 'C'
}
self.assertRaises(RuntimeError, func)
tree.thread_count = 1
tree.params = {
'target' : 'C'
}
self.assertEqual(1, tree.thread_count)
self.assertEqual(tree.params['source'], mol1)
self.assertEqual(tree.params['target'], 'C')
self.assertEqual(tree.params['operators'], params.param_dict['operators']) # we should still have the same opers set
tree.params = params; tree.thread_count = 0 # assign the original parameters back
self.assertEqual(0, tree.thread_count)
self.assertEqual(tree.params['source'], mol1)
self.assertEqual(tree.params['target'], mol2)
self.assertEqual(tree.params['operators'], params.param_dict['operators'])
leaf = tree.leaves[0]
self.assertRaises(RuntimeError, lambda : leaf.setSMILES('CCCC'))
self.assertTrue(tree.hasMol(leaf))
# self.assertEqual(tree, leaf.tree) # FIXME: add a reliable operator for comparison between trees
leaf.setDistToTarget(0.5)
self.assertEqual(tree.leaves[0].getDistToTarget(), 0.5)
leaf_copy = tree.leaves[0].copy()
# self.assertFalse(tree.hasMol(leaf_copy)) # FIXME: add a reliable operator for comparison between trees (this should check both the SMILES and the tree ownership)
self.assertEqual(leaf_copy.getDistToTarget(), 0.5)
leaf_copy.setDistToTarget(0.7)
self.assertEqual(leaf.getDistToTarget(), 0.5)
self.assertEqual(tree.leaves[0].getDistToTarget(), 0.5)
self.assertEqual(leaf_copy.getDistToTarget(), 0.7)
if __name__ == "__main__":
unittest.main()