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.
Files changed (68) hide show
  1. vantami/__init__.py +12 -0
  2. vantami/api/__init__.py +0 -0
  3. vantami/api/convert.py +45 -0
  4. vantami/api/resolve.py +290 -0
  5. vantami/cache.py +14 -0
  6. vantami/chemistry/__init__.py +0 -0
  7. vantami/chemistry/molecule.py +273 -0
  8. vantami/cli/CDDD.py +63 -0
  9. vantami/cli/CIR.py +185 -0
  10. vantami/cli/Dock.py +215 -0
  11. vantami/cli/MolStandardizer.py +635 -0
  12. vantami/cli/Mordred.py +76 -0
  13. vantami/cli/OptunaOptimization.py +151 -0
  14. vantami/cli/__init__.py +0 -0
  15. vantami/data/__init__.py +0 -0
  16. vantami/data/cluster.py +230 -0
  17. vantami/data/descriptors.py +1076 -0
  18. vantami/data/distance.py +539 -0
  19. vantami/data/filter.py +69 -0
  20. vantami/data/manager.py +309 -0
  21. vantami/data/manipulate.py +94 -0
  22. vantami/data/partition.py +659 -0
  23. vantami/data/transform.py +606 -0
  24. vantami/deep/__init__.py +5 -0
  25. vantami/deep/dataset.py +252 -0
  26. vantami/deep/loader.py +172 -0
  27. vantami/deep/models.py +625 -0
  28. vantami/deep/modules.py +704 -0
  29. vantami/deep/utils.py +28 -0
  30. vantami/deep/vectorizer.py +389 -0
  31. vantami/io/__init__.py +2 -0
  32. vantami/io/database.py +303 -0
  33. vantami/io/file.py +99 -0
  34. vantami/metrics/__init__.py +0 -0
  35. vantami/metrics/modellability.py +30 -0
  36. vantami/ml/__init__.py +0 -0
  37. vantami/ml/augood.py +227 -0
  38. vantami/ml/evaluate.py +151 -0
  39. vantami/ml/models.py +617 -0
  40. vantami/ml/optimize.py +319 -0
  41. vantami/ml/params.py +331 -0
  42. vantami/ml/score.py +170 -0
  43. vantami/ml/select.py +150 -0
  44. vantami/ml/utils.py +125 -0
  45. vantami/nlp/__init__.py +0 -0
  46. vantami/nlp/article.py +197 -0
  47. vantami/nlp/cluster.py +32 -0
  48. vantami/nlp/tokenize.py +60 -0
  49. vantami/standardize/__init__.py +0 -0
  50. vantami/standardize/clean.py +853 -0
  51. vantami/standardize/duplicates.py +338 -0
  52. vantami/standardize/filter.py +699 -0
  53. vantami/standardize/validate.py +105 -0
  54. vantami/stats/__init__.py +0 -0
  55. vantami/stats/tests.py +111 -0
  56. vantami/stats/utils.py +15 -0
  57. vantami/visualize/__init__.py +0 -0
  58. vantami/visualize/ecdf.py +79 -0
  59. vantami/visualize/embedding.py +93 -0
  60. vantami/visualize/performance.py +26 -0
  61. vantami/visualize/predictions.py +352 -0
  62. vantami/visualize/properties.py +322 -0
  63. vantami/visualize/utils.py +241 -0
  64. vantami-0.4.3.dist-info/METADATA +143 -0
  65. vantami-0.4.3.dist-info/RECORD +68 -0
  66. vantami-0.4.3.dist-info/WHEEL +5 -0
  67. vantami-0.4.3.dist-info/licenses/LICENSE +28 -0
  68. 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
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