1
"""
2
mol_toolkit is a set of factory methods which
3
allow the user to interact with ChemPer Mol, Atom, and Bond
4
objects without having to explicitly define which toolkit they
5
are using.
6

7
For example one user could be loading their molecules through openeye
8
and the others could be using RDKit to create what we would call a "user mol"
9

10
In this case they can call:
11
from chemper.mol_toolkits import mol_toolkit
12
mol = mol_toolkit.Mol(user_mol)
13
without needing to tell us what toolkit they are using or they
14
could specify
15
mol = mol_toolkit.Mol(user_mol, toolkit='openeye')
16

17
"""
18 100
import os
19 100
from chemper.chemper_utils import get_data_path
20 100
from chemper.mol_toolkits.adapters import MolAdapter, BondAdapter, AtomAdapter
21

22
# identify which toolkits are available
23 100
HAS_OE = False
24 100
HAS_RDK = False
25 100
try:
26 100
    from openeye import oechem
27 41
    if oechem.OEChemIsLicensed():
28 41
        from chemper.mol_toolkits import cp_openeye
29 41
        HAS_OE = True
30 59
except ImportError:
31 59
    HAS_OE = False
32

33 100
try:
34 100
    from rdkit import Chem
35 72
    from chemper.mol_toolkits import cp_rdk
36 72
    HAS_RDK = True
37 28
except ImportError:
38 28
    HAS_RDK = False
39

40

41
# ======================================================================
42
# Find super Mol/Atom/Bond classes
43
# ======================================================================
44

45 100
if not HAS_OE and not HAS_RDK:
46 0
    raise ImportWarning("No Cheminformatics toolkit was found." \
47
                        " currently ChemPer supports OpenEye and RDKit")
48

49

50 100
class Mol(MolAdapter):
51
    # TODO: This is a really interesting implementation. It basically says "I inherit from MolAdapter, and -- trust me
52
    #  -- whatever you get out of this will have all the MolAdapter functionality implemented". The class is basically
53
    #  just a switchboard to figure out which OTHER MolAdapter subclass is appropriate to the input. This design pattern
54
    #  makes me nervous because it would seem to break a contract with the user, where they call one class's __init__
55
    #  function, but receive an object of a different class.
56
    #  Alternatives to this would include
57
    #       * Having each MolAdapter subclass have a `from_object` method, similar to OFFTK. Then, the MolAdapter
58
    #         constructor could iterate over all subclasses (using something like an `all_subclasses` method) and
59
    #         try creating each class from the input, until one succeeds.
60
    #       * Just use the OFF Molecule class, which avoids this trouble by copying the data OUT of the OEMol or RDMol
61
    #         into a toolkit-independent format (OFFMol). Then, when manipulation operations occur, the OFFMol is
62
    #         converted back into the appropriate toolkit molecule, and the operation happens natively there. I think
63
    #         that smirks_search is the only function that would need to be ported over if that was done.
64

65 100
    def __init__(self, mol):
66
        # check if its a ChemPer Mol with OE wrapper
67 100
        if HAS_OE and isinstance(mol, cp_openeye.Mol):
68 41
            self.mol = mol.mol
69 41
            self.__class__ = cp_openeye.Mol
70

71
        # check if this is an Openeye molecule
72 100
        elif HAS_OE and isinstance(mol, oechem.OEMolBase):
73 0
            self.__class__ = cp_openeye.Mol
74 0
            self.__class__.__init__(self,mol)
75

76
        # check if its a ChemPer Mol with RDK wrapper
77 100
        elif HAS_RDK and isinstance(mol, cp_rdk.Mol):
78 59
            self.mol = mol.mol
79 59
            self.__class__ = cp_rdk.Mol
80

81
        # check if it is an RDK molecule
82 100
        elif HAS_RDK and isinstance(mol, Chem.rdchem.Mol):
83 0
            self.__class__ = cp_rdk.Mol
84 0
            self.__class__.__init__(self, mol)
85

86
        else:
87 100
            err_msg = """
88
            Your molecule has the type %s.
89
            Currently ChemPer only supports OpenEye and RDKit.
90
            To add support to a new toolkit submit an issue on GitHub at
91
            github.com/MobleyLab/chemper
92
            """
93 100
            raise TypeError(err_msg % type(mol))
94

95 100
    @staticmethod
96
    def from_smiles(smiles):
97 100
        if HAS_OE:
98 41
            return cp_openeye.Mol.from_smiles(smiles)
99 59
        return cp_rdk.Mol.from_smiles(smiles)
100

101

102 100
    def set_aromaticity_mdl(self):
103
        """
104
        Sets the aromaticity flags in this molecule to use the MDL model
105
        """
106 0
        raise NotImplementedError()
107

108 100
    def get_atoms(self):
109
        """
110
        Returns
111
        -------
112
        atom_list : list[ChemPer Atoms]
113
            list of all atoms in the molecule
114
        """
115 0
        raise NotImplementedError()
116

117 100
    def get_atom_by_index(self, idx):
118
        """
119
        Parameters
120
        ----------
121
        idx : int
122
            atom index
123

124
        Returns
125
        -------
126
        atom : ChemPer Atom
127
            atom with index idx
128
        """
129 0
        raise NotImplementedError()
130

131 100
    def get_bonds(self):
132
        """
133
        Returns
134
        -------
135
        bond_list : list[ChemPer Bonds]
136
            list of all bonds in the molecule
137
        """
138 0
        raise NotImplementedError()
139

140 100
    def get_bond_by_index(self, idx):
141
        """
142
        Parameters
143
        ----------
144
        idx: int
145
            bond index
146

147
        Returns
148
        -------
149
        bond: ChemPer Bond
150
            bond with index idx
151
        """
152 0
        raise NotImplementedError()
153

154 100
    def get_bond_by_atoms(self, atom1, atom2):
155
        """
156
        Finds a bond between two atoms
157

158
        Parameters
159
        ----------
160
        atom1 : ChemPer Atom
161
        atom2 : ChemPer Atom
162

163
        Returns
164
        -------
165
        bond : ChemPer Bond or None
166
            If atoms are connected returns bond otherwise None
167
        """
168 0
        raise NotImplementedError()
169

170 100
    def smirks_search(self, smirks):
171
        """
172
        Performs a substructure search on the molecule with the provided
173
        SMIRKS pattern. Note - this function expects SMIRKS patterns with indexed atoms
174
        that is with :n for at least some atoms.
175

176
        Parameters
177
        ----------
178
        smirks : str
179
            SMIRKS pattern with indexed atoms (:n)
180

181
        Returns
182
        -------
183
        matches : list[match dictionary]
184
            match dictionaries have the form {smirks index: atom index}
185
        """
186 0
        raise NotImplementedError()
187

188 100
    def get_smiles(self):
189
        """
190
        Returns
191
        -------
192
        smiles: str
193
            SMILES string for the molecule
194
        """
195 0
        raise NotImplementedError()
196

197 100
class Atom:
198 100
    def __init__(self, atom):
199

200 100
        if HAS_OE and isinstance(atom, cp_openeye.Atom):
201 0
            self.atom = atom.atom
202 0
            self.__class__ = cp_openeye.Atom
203

204 100
        elif HAS_OE and isinstance(atom, oechem.OEAtomBase):
205 0
            self.__class__ = cp_openeye.Atom
206 0
            self.__class__.__init__(self, atom)
207

208 100
        elif HAS_RDK and isinstance(atom, cp_rdk.Atom):
209 0
            self.atom = atom.atom
210 0
            self.__class__ = cp_rdk.Atom
211

212 100
        elif HAS_RDK and isinstance(atom, Chem.rdchem.Atom):
213 0
            self.__class__ = cp_rdk.Atom
214 0
            self.__class__.__init__(self, atom)
215

216
        else:
217 100
            err_msg = """
218
            Your atom has the type %s.
219
            Currently ChemPer only supports OpenEye and RDKit.
220
            To add support to a new toolkit submit an issue on GitHub at
221
            github.com/MobleyLab/chemper
222
            """
223 100
            raise TypeError(err_msg % type(atom))
224

225

226 100
class Bond:
227 100
    def __init__(self, bond):
228 100
        if HAS_OE and isinstance(bond, cp_openeye.Bond):
229 0
            self.bond = bond.bond
230 0
            self.__class__ = cp_openeye.Bond
231

232 100
        elif HAS_OE and isinstance(bond, oechem.OEBondBase):
233 0
            self.__class__ = cp_openeye.Bond
234 0
            self.__class__.__init__(self,bond)
235

236 100
        elif HAS_RDK and isinstance(bond, cp_rdk.Bond):
237 0
            self.__class__ = cp_rdk.Bond
238 0
            self.bond = bond.bond
239

240 100
        elif HAS_RDK and isinstance(bond, Chem.rdchem.Bond):
241 0
            self.__class__ = cp_rdk.Bond
242 0
            self.__class__.__init__(self,bond)
243

244
        else:
245 100
            err_msg = """
246
            Your bond has the type %s.
247
            Currently ChemPer only supports OpenEye and RDKit.
248
            To add support to a new toolkit submit an issue on GitHub at
249
            github.com/MobleyLab/chemper
250
            """
251 100
            raise TypeError(err_msg % type(bond))
252

253

254
# =======================================
255
# check user specifications
256
# =======================================
257

258 100
def check_toolkit(toolkit=None):
259
    """
260

261
    Parameters
262
    ----------
263
    toolkit : str or None
264
              'openeye', 'rdkit', or None
265
              if None then the toolkit will be picked automatically
266

267
    Returns
268
    -------
269
    toolkit : str
270
              returns the name of the toolkit to be used.
271
              If the package isn't available for the specified toolkit
272
              then an error is raised instead
273
    """
274
    # check for a stable
275 100
    if toolkit is None:
276 100
        if HAS_OE:
277 41
            return 'openeye'
278 59
        elif HAS_RDK:
279 59
            return 'rdkit'
280

281 100
    if toolkit.lower() == 'openeye' and HAS_OE:
282 41
        return 'openeye'
283

284 100
    if toolkit.lower() == 'rdkit' and HAS_RDK:
285 72
        return 'rdkit'
286

287 100
    if toolkit.lower() == 'openeye' or toolkit.lower() == 'rdkit':
288 87
        raise ImportError("Toolkit (%s) was not importable" % toolkit)
289

290
    else:
291 100
        raise ImportError("The provided toolkit (%s) is not supported,"\
292
                          " ChemPer only supports 'openeye' and 'rdkit'" \
293
                          % toolkit)
294

295

296 100
def check_mol_file(file_name):
297
    """
298

299
    Parameters
300
    ----------
301
    file_name : str
302
                path to a molecule file
303

304
    Returns
305
    -------
306
    path : str
307
           absolute path to a molecule file
308
           raises error if file isn't available
309
    """
310
    # is it a local file?
311 100
    if os.path.exists(file_name):
312 100
        return os.path.abspath(file_name)
313

314 100
    path = get_data_path(os.path.join('molecules', file_name))
315

316 100
    if not os.path.exists(path):
317 0
        raise IOError("Molecule file (%s) was not found locally or in chemper/data/molecules" % file_name)
318

319 100
    return path
320

321

322
# =======================================
323
# get molecules from files
324
# =======================================
325

326 100
def mols_from_mol2(mol2_file, toolkit=None):
327
    """
328
    Creates a list of ChemPer Mols from the provided mol2 file
329
    using a specified or default toolkit
330

331
    Parameters
332
    ----------
333
    mol2_file : str
334
                path to mol2 file, this can be a relative or absolute path locally
335
                or the path to a molecules file stored in ChemPer at chemper/data/molecules/
336
    toolkit : None or str
337
              'openeye' or 'rdkit' are the two supported toolkits
338
              if None then the first package available (in the order listed here)
339
              will be used
340

341
    Returns
342
    -------
343
    mol2s : list[ChemPer Mol]
344
            List of molecules in the provided mol2 file
345
    """
346 100
    toolkit = check_toolkit(toolkit)
347 100
    mol2_path = check_mol_file(mol2_file)
348

349 100
    if toolkit.lower() == 'openeye':
350 41
        return cp_openeye.mols_from_mol2(mol2_path)
351

352 72
    return cp_rdk.mols_from_mol2(mol2_path)

Read our documentation on viewing source code .

Loading