vantami 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vantami/__init__.py +12 -0
- vantami/api/__init__.py +0 -0
- vantami/api/convert.py +45 -0
- vantami/api/resolve.py +290 -0
- vantami/cache.py +14 -0
- vantami/chemistry/__init__.py +0 -0
- vantami/chemistry/molecule.py +273 -0
- vantami/cli/CDDD.py +63 -0
- vantami/cli/CIR.py +185 -0
- vantami/cli/Dock.py +215 -0
- vantami/cli/MolStandardizer.py +635 -0
- vantami/cli/Mordred.py +76 -0
- vantami/cli/OptunaOptimization.py +151 -0
- vantami/cli/__init__.py +0 -0
- vantami/data/__init__.py +0 -0
- vantami/data/cluster.py +230 -0
- vantami/data/descriptors.py +1076 -0
- vantami/data/distance.py +539 -0
- vantami/data/filter.py +69 -0
- vantami/data/manager.py +309 -0
- vantami/data/manipulate.py +94 -0
- vantami/data/partition.py +659 -0
- vantami/data/transform.py +606 -0
- vantami/deep/__init__.py +5 -0
- vantami/deep/dataset.py +252 -0
- vantami/deep/loader.py +172 -0
- vantami/deep/models.py +625 -0
- vantami/deep/modules.py +704 -0
- vantami/deep/utils.py +28 -0
- vantami/deep/vectorizer.py +389 -0
- vantami/io/__init__.py +2 -0
- vantami/io/database.py +303 -0
- vantami/io/file.py +99 -0
- vantami/metrics/__init__.py +0 -0
- vantami/metrics/modellability.py +30 -0
- vantami/ml/__init__.py +0 -0
- vantami/ml/augood.py +227 -0
- vantami/ml/evaluate.py +151 -0
- vantami/ml/models.py +617 -0
- vantami/ml/optimize.py +319 -0
- vantami/ml/params.py +331 -0
- vantami/ml/score.py +170 -0
- vantami/ml/select.py +150 -0
- vantami/ml/utils.py +125 -0
- vantami/nlp/__init__.py +0 -0
- vantami/nlp/article.py +197 -0
- vantami/nlp/cluster.py +32 -0
- vantami/nlp/tokenize.py +60 -0
- vantami/standardize/__init__.py +0 -0
- vantami/standardize/clean.py +853 -0
- vantami/standardize/duplicates.py +338 -0
- vantami/standardize/filter.py +699 -0
- vantami/standardize/validate.py +105 -0
- vantami/stats/__init__.py +0 -0
- vantami/stats/tests.py +111 -0
- vantami/stats/utils.py +15 -0
- vantami/visualize/__init__.py +0 -0
- vantami/visualize/ecdf.py +79 -0
- vantami/visualize/embedding.py +93 -0
- vantami/visualize/performance.py +26 -0
- vantami/visualize/predictions.py +352 -0
- vantami/visualize/properties.py +322 -0
- vantami/visualize/utils.py +241 -0
- vantami-0.4.3.dist-info/METADATA +143 -0
- vantami-0.4.3.dist-info/RECORD +68 -0
- vantami-0.4.3.dist-info/WHEEL +5 -0
- vantami-0.4.3.dist-info/licenses/LICENSE +28 -0
- vantami-0.4.3.dist-info/top_level.txt +1 -0
vantami/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from vantami import api
|
|
2
|
+
from vantami import chemistry
|
|
3
|
+
from vantami import cli
|
|
4
|
+
from vantami import data
|
|
5
|
+
from vantami import deep
|
|
6
|
+
from vantami import io
|
|
7
|
+
from vantami import metrics
|
|
8
|
+
from vantami import nlp
|
|
9
|
+
from vantami import standardize
|
|
10
|
+
from vantami import ml
|
|
11
|
+
from vantami import stats
|
|
12
|
+
from vantami import visualize
|
vantami/api/__init__.py
ADDED
|
File without changes
|
vantami/api/convert.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import polars as pl
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def opsin_convert(df: pl.DataFrame, opsin_paths: str, name_col: str = 'Name', out_col: str = 'SMILES'):
|
|
8
|
+
"""
|
|
9
|
+
Convert IUPAC names to SMILES
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
|
|
14
|
+
df: pl.DataFrame
|
|
15
|
+
A polars DataFrame
|
|
16
|
+
opsin_paths: str
|
|
17
|
+
Path to a JSON file containing paths for OPSIN-related files. Must contain path to opsin .jar file ['opsin']
|
|
18
|
+
and to a directory with read/write permission ['io']
|
|
19
|
+
name_col: str
|
|
20
|
+
Name of the column with IUPAC names of chemicals
|
|
21
|
+
out_col: str
|
|
22
|
+
Name of the column for output
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
with open(opsin_paths, 'r') as file:
|
|
26
|
+
paths = json.load(file)
|
|
27
|
+
|
|
28
|
+
in_path = os.path.join(paths['io'], 'names_in.txt')
|
|
29
|
+
out_path = os.path.join(paths['io'], 'names_out.txt')
|
|
30
|
+
|
|
31
|
+
names = df[name_col].to_list()
|
|
32
|
+
|
|
33
|
+
with open(in_path, 'w') as f:
|
|
34
|
+
for name in names:
|
|
35
|
+
f.write(name + '\n')
|
|
36
|
+
|
|
37
|
+
open(out_path, 'w')
|
|
38
|
+
|
|
39
|
+
command = f"java -jar {paths['opsin']} -osmi {in_path} {out_path}"
|
|
40
|
+
os.system(command)
|
|
41
|
+
|
|
42
|
+
smiles = open(out_path, 'r').readlines()
|
|
43
|
+
|
|
44
|
+
df = df.with_columns(pl.Series(out_col, smiles))
|
|
45
|
+
return df
|
vantami/api/resolve.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
from typing import Union, List
|
|
2
|
+
import numpy as np
|
|
3
|
+
import time
|
|
4
|
+
from urllib.request import urlopen
|
|
5
|
+
from urllib.error import HTTPError, URLError
|
|
6
|
+
import pubchempy as pcp
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cactus_resolver(name: str, resolver: str = 'smiles', sleep_time: int = 6, max_retries: int = 3, verbose: int = 0):
|
|
11
|
+
"""
|
|
12
|
+
Resolve chemical identity of a single molecule using NCI/NIH CIR API
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
---------------
|
|
16
|
+
name : str
|
|
17
|
+
Name of a molecule
|
|
18
|
+
resolver : str
|
|
19
|
+
Type of output. available are: smiles, stdinchi, stdinchikey, cas, iupac_name
|
|
20
|
+
sleep_time : int
|
|
21
|
+
Time to wait between attempting a retry
|
|
22
|
+
max_retries : int
|
|
23
|
+
Maximum number of attempts to make
|
|
24
|
+
verbose : int
|
|
25
|
+
Option to control verbosity level. Set to 0 for no logging.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
name = name.lower().strip().replace(' ', '%20')
|
|
29
|
+
url = f'https://cactus.nci.nih.gov/chemical/structure/{name.lower()}/{resolver.lower()}'
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
out = urlopen(url, data=None, timeout=300).read().decode()
|
|
33
|
+
return out
|
|
34
|
+
except HTTPError as e:
|
|
35
|
+
if verbose > 0:
|
|
36
|
+
print(f"Failed to resolve [{name.replace('%20', ' ')}] due to {e}")
|
|
37
|
+
return np.nan
|
|
38
|
+
except URLError or ConnectionResetError as e:
|
|
39
|
+
if verbose > 0:
|
|
40
|
+
print(f"Connection lost while resolving [{name.replace('%20', ' ')}] "
|
|
41
|
+
f"due to {e}. Retrying in {sleep_time} seconds.")
|
|
42
|
+
retries = 1
|
|
43
|
+
while retries <= max_retries:
|
|
44
|
+
try:
|
|
45
|
+
time.sleep(sleep_time)
|
|
46
|
+
sleep_time += 2
|
|
47
|
+
out = urlopen(url, data=None).read().decode()
|
|
48
|
+
if verbose > 0:
|
|
49
|
+
print(f"[{name.replace('%20', ' ')}] solved successfully")
|
|
50
|
+
return out
|
|
51
|
+
except HTTPError as e:
|
|
52
|
+
if verbose > 0:
|
|
53
|
+
print(f"Failed to resolve [{name.replace('%20', ' ')}] "
|
|
54
|
+
f"after {retries} attempts due to {e}")
|
|
55
|
+
return np.nan
|
|
56
|
+
except (URLError, ConnectionResetError) as e:
|
|
57
|
+
if verbose > 0:
|
|
58
|
+
print(f"Connection lost while resolving [{name.replace('%20', ' ')}] "
|
|
59
|
+
f"due to {e}. Retrying in {sleep_time} seconds.")
|
|
60
|
+
retries += 1
|
|
61
|
+
if verbose > 0:
|
|
62
|
+
print(f"Failed to resolve [{name.replace('%20', ' ')}] after {max_retries} attempts")
|
|
63
|
+
return np.nan
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def pubchem_resolver(name: str, resolver: Union [str, list], sleep_time: int = 6, max_retries: int = 3):
|
|
67
|
+
"""
|
|
68
|
+
Resolve chemical identity of a single molecule using PubChem API
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
---------------
|
|
72
|
+
name : str
|
|
73
|
+
name of a molecule
|
|
74
|
+
resolver : Union[str | list]
|
|
75
|
+
type of output. available are: smiles, stdinchi, stdinchikey, cas, iupac_name
|
|
76
|
+
sleep_time : int
|
|
77
|
+
time to wait between attempting a retry
|
|
78
|
+
max_retries : int
|
|
79
|
+
maximum number of attempts to make
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
name = name.lower().strip().replace(' ', '%20')
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
out = pcp.get_properties(resolver, name, 'name')
|
|
86
|
+
if out:
|
|
87
|
+
return out[0]
|
|
88
|
+
else:
|
|
89
|
+
return {}
|
|
90
|
+
except HTTPError as e:
|
|
91
|
+
print(f"Failed to resolve [{name.replace('%20', ' ')}] due to {e}")
|
|
92
|
+
return {}
|
|
93
|
+
except URLError or ConnectionResetError as e:
|
|
94
|
+
print(
|
|
95
|
+
f"Connection lost while resolving [{name.replace('%20', ' ')}] due to {e}. "
|
|
96
|
+
f"Retrying in {sleep_time} seconds.")
|
|
97
|
+
retries = 1
|
|
98
|
+
while retries <= max_retries:
|
|
99
|
+
try:
|
|
100
|
+
time.sleep(sleep_time)
|
|
101
|
+
sleep_time += 2
|
|
102
|
+
out = pcp.get_properties(resolver, name, 'name')
|
|
103
|
+
if out:
|
|
104
|
+
print(f"[{name.replace('%20', ' ')}] solved successfully")
|
|
105
|
+
return out
|
|
106
|
+
else:
|
|
107
|
+
return {}
|
|
108
|
+
except HTTPError as e:
|
|
109
|
+
print(f"Failed to resolve [{name.replace('%20', ' ')}] after {retries} attempts due to {e}")
|
|
110
|
+
return {}
|
|
111
|
+
except (URLError, ConnectionResetError) as e:
|
|
112
|
+
print(f"Connection lost while resolving [{name.replace('%20', ' ')}] due to {e}. "
|
|
113
|
+
f"Retrying in {sleep_time} seconds.")
|
|
114
|
+
pass
|
|
115
|
+
retries += 1
|
|
116
|
+
print(f"Failed to resolve [{name.replace('%20', ' ')}] after {max_retries} attempts")
|
|
117
|
+
return {}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def commonchemistry_resolver(cas, parser):
|
|
121
|
+
url = f'https://commonchemistry.cas.org/detail?cas_rn={cas}'
|
|
122
|
+
try:
|
|
123
|
+
out = urlopen(url, timeout=30).read().decode('utf-8')
|
|
124
|
+
except:
|
|
125
|
+
pass
|
|
126
|
+
raise NotImplementedError
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
"def process_cas(cas, parser):\n",
|
|
131
|
+
" if not 'NOCAS' in cas:\n",
|
|
132
|
+
" url = f'https://commonchemistry.cas.org/detail?cas_rn={cas}'\n",
|
|
133
|
+
" try:\n",
|
|
134
|
+
" out = urlopen(url, data=None, timeout=3600).read().decode('utf-8')\n",
|
|
135
|
+
" root = html.fromstring(out, parser=parser)\n",
|
|
136
|
+
" try:\n",
|
|
137
|
+
" for child in root.find('./body/app-root/app-detail/ngx-json-ld'):\n",
|
|
138
|
+
" dc = ast.literal_eval(child.text)['hasBioChemEntityPart']['identifier']\n",
|
|
139
|
+
" if re.search(r'\\AInChI', dc) is not None:\n",
|
|
140
|
+
" mol = MolFromInchi(dc)\n",
|
|
141
|
+
" smiles = Chem.MolToSmiles(mol)\n",
|
|
142
|
+
" return smiles \n",
|
|
143
|
+
" except Exception as e:\n",
|
|
144
|
+
" print(e)\n",
|
|
145
|
+
" return np.nan\n",
|
|
146
|
+
" except (HTTPError, URLError, ConnectionResetError, UnicodeEncodeError):\n",
|
|
147
|
+
" return np.nan"
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def proxy_cactus_resolver(names: Union[str, List[str]], proxies: dict, resolver: str = 'smiles',
|
|
152
|
+
timeout: int = 6, verbose: int = 0) -> dict:
|
|
153
|
+
"""
|
|
154
|
+
Resolve chemical identity of a molecule using NCI/NIH CIR API.
|
|
155
|
+
|
|
156
|
+
Uses the Chemical Identifier Resolver (CIR) web service to convert various
|
|
157
|
+
chemical identifiers to the specified format.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
names: str
|
|
162
|
+
Name of the molecule to resolve (or a list of names).
|
|
163
|
+
proxies: dict
|
|
164
|
+
Dictionary containing proxy configuration for HTTP/HTTPS requests
|
|
165
|
+
resolver: str, optional
|
|
166
|
+
Output format requested. Available options: 'smiles', 'stdinchi',
|
|
167
|
+
'stdinchikey', 'cas', 'iupac_name'. Default is 'smiles'.
|
|
168
|
+
timeout: int, optional
|
|
169
|
+
Request timeout in seconds. Default is 6.
|
|
170
|
+
verbose: int, optional
|
|
171
|
+
Verbosity level. Set to 0 for silent operation. Default is 0.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
str or None
|
|
176
|
+
The resolved chemical identifier in requested format if successful,
|
|
177
|
+
None if resolution failed.
|
|
178
|
+
|
|
179
|
+
Notes
|
|
180
|
+
-----
|
|
181
|
+
Author: Mateusz Iwan
|
|
182
|
+
Email: mateusz.iwan@bayer.com / mateusz.iwan@hotmail.com
|
|
183
|
+
Environment: Technically any
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
resolver = resolver.lower().strip()
|
|
187
|
+
|
|
188
|
+
if not isinstance(names, (str, list)): # check type
|
|
189
|
+
raise TypeError('Argument < names > must be a string or a list of strings')
|
|
190
|
+
if isinstance(names, list) and not all(isinstance(name, str) for name in names):
|
|
191
|
+
raise TypeError('When < names > is a list, all elements must be strings')
|
|
192
|
+
|
|
193
|
+
input_names = [names] if isinstance(names, str) else names
|
|
194
|
+
|
|
195
|
+
outputs = {}
|
|
196
|
+
session = requests.Session()
|
|
197
|
+
|
|
198
|
+
for orig_name in input_names:
|
|
199
|
+
name = orig_name.lower().strip().replace(' ', '%20')
|
|
200
|
+
url = f'https://cactus.nci.nih.gov/chemical/structure/{name}/{resolver}'
|
|
201
|
+
try:
|
|
202
|
+
response = session.get(url, timeout=timeout, proxies=proxies)
|
|
203
|
+
if (code := response.status_code) != 200:
|
|
204
|
+
if verbose > 0:
|
|
205
|
+
print(f'Failed with status code {code} for {orig_name}')
|
|
206
|
+
outputs[orig_name] = None
|
|
207
|
+
continue
|
|
208
|
+
outputs[orig_name] = response.text.strip()
|
|
209
|
+
|
|
210
|
+
except requests.exceptions.RequestException as e:
|
|
211
|
+
if verbose > 0:
|
|
212
|
+
print(f'Request exception: {e}')
|
|
213
|
+
outputs[orig_name] = None
|
|
214
|
+
return outputs
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def proxy_pubchem_resolver(idxs: Union[str, List[str]], proxies: dict,
|
|
218
|
+
idx_type: str, timeout: int = 6, verbose: int = 0):
|
|
219
|
+
"""
|
|
220
|
+
Retrieve all available properties for a compound from PubChem using its name or CID.
|
|
221
|
+
Passing multiple values is supported.
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
idxs: Union[str, List[str]]
|
|
226
|
+
Name(s) of the compound or PubChem CID(s)
|
|
227
|
+
proxies : dict
|
|
228
|
+
Dictionary containing proxy configuration for HTTP/HTTPS requests
|
|
229
|
+
idx_type: str
|
|
230
|
+
'cid' or 'name'
|
|
231
|
+
timeout : int, optional
|
|
232
|
+
Request timeout in seconds. Default is 6.
|
|
233
|
+
verbose : int, optional
|
|
234
|
+
Verbosity level. Set to 0 for silent operation. Default is 0.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
dict
|
|
239
|
+
Dictionary containing all available properties
|
|
240
|
+
|
|
241
|
+
Notes
|
|
242
|
+
-----
|
|
243
|
+
Author: Mateusz Iwan
|
|
244
|
+
Email: mateusz.iwan@bayer.com / mateusz.iwan@hotmail.com
|
|
245
|
+
Environment: Any # technically
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
if not isinstance(idxs, (str, list)): # check type
|
|
249
|
+
raise TypeError('Argument < names > must be a string or a list of strings')
|
|
250
|
+
if isinstance(idxs, list) and not all(isinstance(idx, str) for idx in idxs):
|
|
251
|
+
raise TypeError('When < names > is a list, all elements must be strings')
|
|
252
|
+
|
|
253
|
+
property_list = [
|
|
254
|
+
"MolecularFormula", "CanonicalSMILES",
|
|
255
|
+
"IsomericSMILES", "InChI",
|
|
256
|
+
"InChIKey", "IUPACName",
|
|
257
|
+
]
|
|
258
|
+
property_string = ",".join(property_list)
|
|
259
|
+
|
|
260
|
+
input_ids = [idxs] if isinstance(idxs, str) else idxs
|
|
261
|
+
|
|
262
|
+
outputs = {}
|
|
263
|
+
session = requests.Session()
|
|
264
|
+
|
|
265
|
+
for orig_id in input_ids:
|
|
266
|
+
idx = orig_id.lower().strip().replace(' ', '%20')
|
|
267
|
+
url = f'https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/{idx_type}/{idx}/property/{property_string}/JSON'
|
|
268
|
+
try:
|
|
269
|
+
response = session.get(url, timeout=timeout, proxies=proxies)
|
|
270
|
+
if (code := response.status_code) != 200:
|
|
271
|
+
if verbose > 0:
|
|
272
|
+
print(f'Failed with status code {code} for {orig_id}')
|
|
273
|
+
outputs[orig_id] = None
|
|
274
|
+
continue
|
|
275
|
+
data = response.json()
|
|
276
|
+
try:
|
|
277
|
+
properties = data['PropertyTable']['Properties'][0]
|
|
278
|
+
outputs[orig_id] = properties
|
|
279
|
+
|
|
280
|
+
except (KeyError, IndexError) as e:
|
|
281
|
+
if verbose > 0:
|
|
282
|
+
print(f"Error extracting properties: {e}")
|
|
283
|
+
outputs[orig_id] = None
|
|
284
|
+
|
|
285
|
+
except requests.exceptions.RequestException as e:
|
|
286
|
+
if verbose > 0:
|
|
287
|
+
print(f'Request exception: {e}')
|
|
288
|
+
outputs[orig_id] = None
|
|
289
|
+
|
|
290
|
+
return outputs
|
vantami/cache.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
def get_novami_cache_dir() -> Path:
|
|
4
|
+
cache_dir = Path.home() / ".cache" / "novami"
|
|
5
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
6
|
+
return cache_dir
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_chemberta_model_path() -> Path:
|
|
10
|
+
return get_novami_cache_dir() / "ChemBERTa-model.joblib"
|
|
11
|
+
|
|
12
|
+
def get_chemberta_tokenizer_path() -> Path:
|
|
13
|
+
return get_novami_cache_dir() / "ChemBERTa-tokenizer.joblib"
|
|
14
|
+
|
|
File without changes
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
""" File with classes and functions related to computational-chemistry related tasks """
|
|
2
|
+
import copy
|
|
3
|
+
import numpy as np
|
|
4
|
+
from rdkit import Chem
|
|
5
|
+
from rdkit.Chem import Draw
|
|
6
|
+
from rdkit.Chem.AllChem import (ETKDGv3, EmbedMolecule, MMFFOptimizeMolecule,
|
|
7
|
+
MMFFGetMoleculeProperties, MMFFGetMoleculeForceField)
|
|
8
|
+
from rdkit.Chem.MolStandardize import rdMolStandardize
|
|
9
|
+
import prolif as plf
|
|
10
|
+
import os
|
|
11
|
+
import glob
|
|
12
|
+
import re
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import MDAnalysis as mda
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Molecule:
|
|
18
|
+
|
|
19
|
+
def __init__(self, smiles: str, mol_id: str):
|
|
20
|
+
|
|
21
|
+
self.smiles = smiles
|
|
22
|
+
self.mol_id = mol_id
|
|
23
|
+
self.mol = Chem.MolFromSmiles(self.smiles)
|
|
24
|
+
self.dock_params = {}
|
|
25
|
+
self.dock_score = None
|
|
26
|
+
self.protein_path = None
|
|
27
|
+
self.docked_path = None
|
|
28
|
+
self.original_path = None
|
|
29
|
+
self.hydrogen_path = None
|
|
30
|
+
self.split_paths = []
|
|
31
|
+
self.pli_df = None
|
|
32
|
+
self.pli = False
|
|
33
|
+
self.tautomers = None
|
|
34
|
+
|
|
35
|
+
def __str__(self):
|
|
36
|
+
return f'Molecule object of < {self.smiles} >'
|
|
37
|
+
|
|
38
|
+
def __repr__(self):
|
|
39
|
+
return f'Molecule object of < {self.smiles} > with id < {self.mol_id} >'
|
|
40
|
+
|
|
41
|
+
def embed(self, attempts: int = 10):
|
|
42
|
+
"""
|
|
43
|
+
Attempt to find lowest-energy 3D coordinates from 2D molecule.
|
|
44
|
+
"""
|
|
45
|
+
self.mol = Chem.AddHs(self.mol)
|
|
46
|
+
params = ETKDGv3()
|
|
47
|
+
mols = []
|
|
48
|
+
energies = []
|
|
49
|
+
|
|
50
|
+
def optimize_molecule(mol_, conf_id_):
|
|
51
|
+
ex_code = MMFFOptimizeMolecule(mol_, maxIters=500, confId=conf_id_)
|
|
52
|
+
match ex_code:
|
|
53
|
+
case -1: # optimization not possible, return whatever molecule was embedded
|
|
54
|
+
print('Could not setup the force field for provided molecule')
|
|
55
|
+
return mol_, np.nan
|
|
56
|
+
case 0: # optimization converged, proceed normally
|
|
57
|
+
prop = MMFFGetMoleculeProperties(mol_)
|
|
58
|
+
ff = MMFFGetMoleculeForceField(mol_, prop)
|
|
59
|
+
energy_ = ff.CalcEnergy()
|
|
60
|
+
return mol_, energy_
|
|
61
|
+
case 1: # force field setup correct, but convergence not reached
|
|
62
|
+
_ = MMFFOptimizeMolecule(mol_, maxIters=5000, confId=conf_id_)
|
|
63
|
+
prop = MMFFGetMoleculeProperties(mol_)
|
|
64
|
+
ff = MMFFGetMoleculeForceField(mol_, prop)
|
|
65
|
+
energy_ = ff.CalcEnergy()
|
|
66
|
+
return mol_, energy_
|
|
67
|
+
|
|
68
|
+
for attempt in range(attempts):
|
|
69
|
+
mol = copy.deepcopy(self.mol)
|
|
70
|
+
conf_id = EmbedMolecule(mol, params)
|
|
71
|
+
if conf_id >= 0: # in case of correct embedding the returned number is not negative
|
|
72
|
+
mol, energy = optimize_molecule(mol, conf_id)
|
|
73
|
+
mols.append(mol)
|
|
74
|
+
energies.append(energy)
|
|
75
|
+
else:
|
|
76
|
+
conf_id = EmbedMolecule(mol, maxAttempts=1000, useRandomCoords=True)
|
|
77
|
+
if isinstance(conf_id, int): # check if other embedding works
|
|
78
|
+
mol, energy = optimize_molecule(mol, conf_id)
|
|
79
|
+
mols.append(mol)
|
|
80
|
+
energies.append(energy)
|
|
81
|
+
else:
|
|
82
|
+
return mol # return unoptimized mol
|
|
83
|
+
|
|
84
|
+
if not all([x is np.nan for x in energies]): # if there is at least one correct energy
|
|
85
|
+
min_id = np.nanargmin(energies)
|
|
86
|
+
return mols[min_id] # return mol with the lowest energy
|
|
87
|
+
else:
|
|
88
|
+
print(f'Optimization not possible for < {self.smiles} >')
|
|
89
|
+
mol = copy.deepcopy(self.mol)
|
|
90
|
+
EmbedMolecule(mol)
|
|
91
|
+
return mol # return original molecule
|
|
92
|
+
|
|
93
|
+
def validate_mol(self):
|
|
94
|
+
raise NotImplementedError
|
|
95
|
+
|
|
96
|
+
def show(self):
|
|
97
|
+
return Draw.MolToImage(self.mol, size=(512, 512))
|
|
98
|
+
|
|
99
|
+
def dock(self, dock_params: dict, overwrite: bool = False):
|
|
100
|
+
|
|
101
|
+
output = dock_params['output_dir'].rstrip('/')
|
|
102
|
+
|
|
103
|
+
os.makedirs(output, exist_ok=True) # make dir if it's not there yet
|
|
104
|
+
|
|
105
|
+
mol_in = f'{output}/mol_{self.mol_id}.mol'
|
|
106
|
+
mol_out = f'{output}/mol_{self.mol_id}.mol2'
|
|
107
|
+
|
|
108
|
+
if glob.glob(mol_in) and not overwrite:
|
|
109
|
+
print(f'Molecule file mol_{self.mol_id}.mol already exists.')
|
|
110
|
+
else:
|
|
111
|
+
self.mol = self.embed()
|
|
112
|
+
Chem.MolToMolFile(self.mol, mol_in)
|
|
113
|
+
self.original_path = mol_in
|
|
114
|
+
convert_command = f'obabel -imol {mol_in} -omol2 -O {mol_out}'
|
|
115
|
+
os.system(convert_command)
|
|
116
|
+
|
|
117
|
+
smina = dock_params['smina_path']
|
|
118
|
+
protein = dock_params['protein_path']
|
|
119
|
+
mol2_in = f'{output}/mol_{self.mol_id}.mol2'
|
|
120
|
+
mol2_out = f'{output}/mol_{self.mol_id}_out.mol2'
|
|
121
|
+
log_out = f'{output}/mol_{self.mol_id}_out.log'
|
|
122
|
+
|
|
123
|
+
x_coord = dock_params['x_coord']
|
|
124
|
+
x_size = dock_params['x_size']
|
|
125
|
+
|
|
126
|
+
y_coord = dock_params['y_coord']
|
|
127
|
+
y_size = dock_params['y_size']
|
|
128
|
+
|
|
129
|
+
z_coord = dock_params['z_coord']
|
|
130
|
+
z_size = dock_params['z_size']
|
|
131
|
+
|
|
132
|
+
exh = dock_params['exhaustiveness']
|
|
133
|
+
|
|
134
|
+
dock_command = (f'{smina} -r {protein} -l {mol2_in} --center_x {x_coord} --center_y {y_coord} --center_z {z_coord} '
|
|
135
|
+
f'--size_x {x_size} --size_y {y_size} --size_z {z_size} --exhaustiveness {exh} --out {mol2_out};')
|
|
136
|
+
|
|
137
|
+
os.system(dock_command)
|
|
138
|
+
|
|
139
|
+
output_command = f'{smina} -r {protein} -l {mol2_out} --score_only --log {log_out}'
|
|
140
|
+
|
|
141
|
+
os.system(output_command)
|
|
142
|
+
|
|
143
|
+
output = open(log_out, 'r').read()
|
|
144
|
+
score = np.min([float(score) for score in re.findall(r'Affinity:\s*([\-?+\d.]+)\s+\(kcal/mol\)', output)])
|
|
145
|
+
|
|
146
|
+
with open(log_out, 'a') as file:
|
|
147
|
+
file.write(f'--- Docking parameters ---\n')
|
|
148
|
+
for name, value in dock_params.items():
|
|
149
|
+
file.write(f'< {name} > : < {value} >\n')
|
|
150
|
+
|
|
151
|
+
if isinstance(score, float):
|
|
152
|
+
print(f'Docking successful')
|
|
153
|
+
self.dock_params = dock_params
|
|
154
|
+
self.dock_score = score
|
|
155
|
+
self.protein_path = protein
|
|
156
|
+
self.docked_path = mol2_out
|
|
157
|
+
|
|
158
|
+
def read_dock(self, output_dir: str):
|
|
159
|
+
|
|
160
|
+
log_file = glob.glob(f"{output_dir.rstrip('/')}/mol_{self.mol_id}_out.log")
|
|
161
|
+
|
|
162
|
+
if len(log_file) == 0:
|
|
163
|
+
print('Molecule not yet docked')
|
|
164
|
+
self.dock_score = np.nan
|
|
165
|
+
else:
|
|
166
|
+
output = open(log_file[0], 'r').read()
|
|
167
|
+
self.dock_score = np.min([float(score) for score in re.findall(r'Affinity:\s*([\-?+\d.]+)\s+\(kcal/mol\)', output)])
|
|
168
|
+
self.docked_path = glob.glob(f"{output_dir.rstrip('/')}/mol_{self.mol_id}_out.mol2")[0]
|
|
169
|
+
self.original_path = glob.glob(f"{output_dir.rstrip('/')}/mol_{self.mol_id}.mol")[0]
|
|
170
|
+
output_list = output.split('\n')
|
|
171
|
+
for line in output_list:
|
|
172
|
+
if (matches := re.search(r'\A<\s(.+)\s>\s:\s<\s(.+)\s>\Z', line)) is not None: # new!
|
|
173
|
+
self.dock_params[matches.group(1)] = matches.group(2)
|
|
174
|
+
self.protein_path = self.dock_params['protein_path']
|
|
175
|
+
|
|
176
|
+
def calculate_pli(self, count: bool = False, file_path: str = None):
|
|
177
|
+
"""
|
|
178
|
+
Calculate protein-ligand interaction fingerprint using prolif library.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
count: bool
|
|
183
|
+
Whether to return count fingerprint or bit fingerprint. Defaults to False
|
|
184
|
+
file_path: str
|
|
185
|
+
If not None, use provided path to file instead of self.docked_path
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def split_mol_file(path_):
|
|
189
|
+
"""
|
|
190
|
+
plf has issues accepting conformers from RDKit so the original file is split into separate .mol files,
|
|
191
|
+
and processed individually
|
|
192
|
+
"""
|
|
193
|
+
split_indices_ = []
|
|
194
|
+
file_ = open(path_, 'r').readlines()
|
|
195
|
+
for i_, line_ in enumerate(file_):
|
|
196
|
+
if '*****' in line_: # used to define a new conformation
|
|
197
|
+
split_indices_.append(i_)
|
|
198
|
+
split_indices_.append(len(file_))
|
|
199
|
+
for j_ in range(len(split_indices_)-1):
|
|
200
|
+
new_path_ = path_.rstrip('.mol') + f'_{j_}.mol'
|
|
201
|
+
new_file_ = file_[split_indices_[j_]: split_indices_[j_+1]]
|
|
202
|
+
with open(new_path_, 'w') as f:
|
|
203
|
+
for line_ in new_file_:
|
|
204
|
+
f.write(line_)
|
|
205
|
+
self.split_paths.append(new_path_)
|
|
206
|
+
|
|
207
|
+
if not all([self.dock_score is not None, self.protein_path is not None, self.docked_path is not None]):
|
|
208
|
+
print(f'Molecule not docked successfully')
|
|
209
|
+
else:
|
|
210
|
+
assert self.protein_path.split('.')[-1] == 'pdb' # check protein file extension
|
|
211
|
+
assert self.docked_path.split('.')[-1] == 'mol2' # check molecule file extension
|
|
212
|
+
"""
|
|
213
|
+
# alternative solution using rdkit
|
|
214
|
+
rdkit_protein = Chem.MolFromPDBFile(self.protein_path, removeHs=False) # read protein file
|
|
215
|
+
plf_protein = plf.Molecule(rdkit_protein) # potentially change back to plf.Molecule(rdkit_protein)
|
|
216
|
+
"""
|
|
217
|
+
u = mda.Universe(self.protein_path)
|
|
218
|
+
plf_protein = plf.Molecule.from_mda(u)
|
|
219
|
+
|
|
220
|
+
if file_path is None: # proceed as usual:
|
|
221
|
+
# OpenBabel sometimes would not add hydrogen atoms to .mol2 file, so it's first converted to .mol file
|
|
222
|
+
conversion_path = self.docked_path.rstrip('2')
|
|
223
|
+
self.hydrogen_path = self.docked_path.rstrip('.mol2') + '_H.mol'
|
|
224
|
+
convert_command_1 = f"obabel -imol2 {self.docked_path} -omol -O {conversion_path}"
|
|
225
|
+
convert_command_2 = f'obabel -imol {conversion_path} -omol -O {self.hydrogen_path} -p 7.4'
|
|
226
|
+
os.system(convert_command_1)
|
|
227
|
+
os.system(convert_command_2)
|
|
228
|
+
os.system(f'rm {conversion_path}')
|
|
229
|
+
else: # use manually prepared file; expected to be .mol file with hydrogen atoms
|
|
230
|
+
self.hydrogen_path = file_path
|
|
231
|
+
|
|
232
|
+
split_mol_file(self.hydrogen_path)
|
|
233
|
+
|
|
234
|
+
for i, path in enumerate(self.split_paths):
|
|
235
|
+
try:
|
|
236
|
+
ifp = plf.Fingerprint()
|
|
237
|
+
rdkit_mol = Chem.MolFromMolFile(path, removeHs=False) # read .mol file
|
|
238
|
+
# noinspection PyTypeChecker
|
|
239
|
+
ifp.run_from_iterable([plf.Molecule.from_rdkit(rdkit_mol)], plf_protein) # calculate
|
|
240
|
+
ifp_df = ifp.to_dataframe(index_col='Pose', drop_empty=False, count=count) # pass to dataframe
|
|
241
|
+
ifp_df = pd.melt(ifp_df) # remove multi-indexing
|
|
242
|
+
ifp_df['dock_number'] = i
|
|
243
|
+
|
|
244
|
+
if self.pli_df is None:
|
|
245
|
+
self.pli_df = ifp_df
|
|
246
|
+
else:
|
|
247
|
+
self.pli_df = pd.concat([self.pli_df, ifp_df], axis=0, ignore_index=True)
|
|
248
|
+
|
|
249
|
+
os.system(f'rm {path}')
|
|
250
|
+
|
|
251
|
+
except AttributeError as e:
|
|
252
|
+
print(f'Could not calculate IFP due to:\n{e}')
|
|
253
|
+
|
|
254
|
+
if self.pli_df is not None: # combine and clean dfs
|
|
255
|
+
self.pli_df = self.pli_df.drop(columns=['ligand'])
|
|
256
|
+
self.pli_df['Mol_ID'] = self.mol_id
|
|
257
|
+
self.pli_df['SMILES'] = self.smiles
|
|
258
|
+
self.pli_df = self.pli_df.rename(columns={'protein': 'residue'})
|
|
259
|
+
self.pli = True
|
|
260
|
+
|
|
261
|
+
def enumerate_tautomers(self):
|
|
262
|
+
enumerator = rdMolStandardize.TautomerEnumerator()
|
|
263
|
+
canon = enumerator.canonicalize(self.mol)
|
|
264
|
+
csmi = Chem.MolToSmiles(canon)
|
|
265
|
+
res = [canon]
|
|
266
|
+
tautomers = enumerator.Enumerate(self.mol)
|
|
267
|
+
smis = [Chem.MolToSmiles(x) for x in tautomers]
|
|
268
|
+
stpl = sorted((x, y) for x,y in zip(smis, tautomers) if x != csmi)
|
|
269
|
+
res += [y for x, y in stpl]
|
|
270
|
+
self.tautomers = res
|
|
271
|
+
|
|
272
|
+
def enumerate_conformers(self):
|
|
273
|
+
raise NotImplementedError
|