D4CMPP2 0.4.0__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.
- D4CMPP2/_Data/AGENTS.md +24 -0
- D4CMPP2/_Data/Aqsoldb.csv +9291 -0
- D4CMPP2/_Data/BradleyMP.csv +3042 -0
- D4CMPP2/_Data/Lipophilicity.csv +1131 -0
- D4CMPP2/_Data/README.md +8 -0
- D4CMPP2/_Data/__init__.py +26 -0
- D4CMPP2/_Data/optical.csv +20237 -0
- D4CMPP2/_Data/test.csv +190 -0
- D4CMPP2/__init__.py +16 -0
- D4CMPP2/__main__.py +5 -0
- D4CMPP2/_main.py +500 -0
- D4CMPP2/cli.py +7 -0
- D4CMPP2/exceptions.py +53 -0
- D4CMPP2/grid_search.py +259 -0
- D4CMPP2/network_refer.yaml +160 -0
- D4CMPP2/networks/AFP_model.py +72 -0
- D4CMPP2/networks/AFPwithSolv_model.py +72 -0
- D4CMPP2/networks/DMPNN_model.py +90 -0
- D4CMPP2/networks/DMPNNwithSolv_model.py +89 -0
- D4CMPP2/networks/GAT_model.py +48 -0
- D4CMPP2/networks/GATwithSolv_model.py +63 -0
- D4CMPP2/networks/GCN_model.py +113 -0
- D4CMPP2/networks/GCNwithSolv_model.py +103 -0
- D4CMPP2/networks/GC_model.py +122 -0
- D4CMPP2/networks/ISATPM_model.py +14 -0
- D4CMPP2/networks/ISATPN_model.py +199 -0
- D4CMPP2/networks/ISAT_model.py +90 -0
- D4CMPP2/networks/MPNN_model.py +56 -0
- D4CMPP2/networks/MPNNwithSolv_model.py +72 -0
- D4CMPP2/networks/__init__.py +25 -0
- D4CMPP2/networks/base.py +250 -0
- D4CMPP2/networks/registry.py +187 -0
- D4CMPP2/networks/src/AFP.py +118 -0
- D4CMPP2/networks/src/BiDropout.py +29 -0
- D4CMPP2/networks/src/DMPNN.py +35 -0
- D4CMPP2/networks/src/GAT.py +69 -0
- D4CMPP2/networks/src/GC.py +85 -0
- D4CMPP2/networks/src/GCN.py +71 -0
- D4CMPP2/networks/src/ISAT.py +153 -0
- D4CMPP2/networks/src/Linear.py +49 -0
- D4CMPP2/networks/src/MPNN.py +56 -0
- D4CMPP2/networks/src/SolventLayer.py +62 -0
- D4CMPP2/networks/src/__init__.py +0 -0
- D4CMPP2/networks/src/distGCN.py +21 -0
- D4CMPP2/networks/src/pyg_hetero.py +24 -0
- D4CMPP2/optimize.py +472 -0
- D4CMPP2/src/Analyzer/ISAAnalyzer.py +458 -0
- D4CMPP2/src/Analyzer/ISAPNAnalyzer.py +366 -0
- D4CMPP2/src/Analyzer/ISAwSAnalyzer.py +117 -0
- D4CMPP2/src/Analyzer/MolAnalyzer.py +319 -0
- D4CMPP2/src/Analyzer/__init__.py +54 -0
- D4CMPP2/src/Analyzer/core.py +480 -0
- D4CMPP2/src/Analyzer/factory.py +166 -0
- D4CMPP2/src/Analyzer/interpretation.py +232 -0
- D4CMPP2/src/Analyzer/results.py +101 -0
- D4CMPP2/src/DataManager/Dataset/GraphDataset.py +314 -0
- D4CMPP2/src/DataManager/Dataset/ISAGraphDataset.py +384 -0
- D4CMPP2/src/DataManager/Dataset/__init__.py +0 -0
- D4CMPP2/src/DataManager/GraphGenerator/ISAGraphGenerator.py +223 -0
- D4CMPP2/src/DataManager/GraphGenerator/MolGraphGenerator.py +73 -0
- D4CMPP2/src/DataManager/GraphGenerator/__init__.py +14 -0
- D4CMPP2/src/DataManager/ISADataManager.py +67 -0
- D4CMPP2/src/DataManager/MolDataManager.py +735 -0
- D4CMPP2/src/DataManager/__init__.py +14 -0
- D4CMPP2/src/DataManager/contracts.py +179 -0
- D4CMPP2/src/NetworkManager/ISANetworkManager.py +12 -0
- D4CMPP2/src/NetworkManager/NetworkManager.py +520 -0
- D4CMPP2/src/NetworkManager/__init__.py +14 -0
- D4CMPP2/src/PostProcessor.py +160 -0
- D4CMPP2/src/TrainManager/ISATrainManager.py +26 -0
- D4CMPP2/src/TrainManager/TrainManager.py +254 -0
- D4CMPP2/src/TrainManager/__init__.py +14 -0
- D4CMPP2/src/TrainManager/callbacks.py +119 -0
- D4CMPP2/src/__init__.py +0 -0
- D4CMPP2/src/utils/PATH.py +246 -0
- D4CMPP2/src/utils/__init__.py +0 -0
- D4CMPP2/src/utils/argparser.py +56 -0
- D4CMPP2/src/utils/checkpointing.py +90 -0
- D4CMPP2/src/utils/config_resolution.py +123 -0
- D4CMPP2/src/utils/config_validation.py +370 -0
- D4CMPP2/src/utils/csv_validation.py +105 -0
- D4CMPP2/src/utils/data_quality.py +181 -0
- D4CMPP2/src/utils/featureizer.py +202 -0
- D4CMPP2/src/utils/functional_group.csv +169 -0
- D4CMPP2/src/utils/graph_cache.py +213 -0
- D4CMPP2/src/utils/leaderboard.py +212 -0
- D4CMPP2/src/utils/metrics.py +31 -0
- D4CMPP2/src/utils/module_loader.py +147 -0
- D4CMPP2/src/utils/output.py +80 -0
- D4CMPP2/src/utils/reproducibility.py +70 -0
- D4CMPP2/src/utils/run_manifest.py +175 -0
- D4CMPP2/src/utils/scaler.py +40 -0
- D4CMPP2/src/utils/sculptor.py +713 -0
- D4CMPP2/src/utils/splitting.py +250 -0
- D4CMPP2/src/utils/supportfile_saver.py +94 -0
- D4CMPP2/src/utils/tools.py +156 -0
- D4CMPP2/src/utils/transfer_learning.py +111 -0
- d4cmpp2-0.4.0.dist-info/METADATA +420 -0
- d4cmpp2-0.4.0.dist-info/RECORD +103 -0
- d4cmpp2-0.4.0.dist-info/WHEEL +5 -0
- d4cmpp2-0.4.0.dist-info/entry_points.txt +2 -0
- d4cmpp2-0.4.0.dist-info/licenses/LICENSE +21 -0
- d4cmpp2-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Positive-negative ISA Analyzer implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
import numpy as np
|
|
11
|
+
from matplotlib.colors import LinearSegmentedColormap
|
|
12
|
+
from matplotlib.gridspec import GridSpec
|
|
13
|
+
from PIL import Image
|
|
14
|
+
import rdkit.Chem as Chem
|
|
15
|
+
|
|
16
|
+
from .ISAAnalyzer import ISAAnalyzer, ISAAnalyzer_v2, showAtomHighlight
|
|
17
|
+
from .interpretation import ISAInterpreter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _ISAPNAnalyzerMixin:
|
|
21
|
+
"""ISATPN-only positive/negative score and hidden-feature behavior."""
|
|
22
|
+
|
|
23
|
+
def _configure_isapn_interpreter(self):
|
|
24
|
+
self.data_keys = [
|
|
25
|
+
"prediction",
|
|
26
|
+
"positive",
|
|
27
|
+
"negative",
|
|
28
|
+
"feature_P",
|
|
29
|
+
"feature_N",
|
|
30
|
+
"fragments",
|
|
31
|
+
]
|
|
32
|
+
self._interpreter = ISAInterpreter(
|
|
33
|
+
self._core,
|
|
34
|
+
score_keys=("positive", "negative"),
|
|
35
|
+
feature_keys=("feature_P", "feature_N"),
|
|
36
|
+
score_mode="fragment",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def analyze_rows(self, *args, include_features=True, **kwargs):
|
|
40
|
+
"""Return ISATPN positive/negative scores and optional P/N features."""
|
|
41
|
+
|
|
42
|
+
return self._interpreter.analyze(
|
|
43
|
+
*args, include_features=include_features, **kwargs
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def get_score(self, *args, **kwargs):
|
|
47
|
+
"""Return row-aligned positive and negative ISATPN scores."""
|
|
48
|
+
|
|
49
|
+
analysis = self.analyze_rows(*args, include_features=False, **kwargs)
|
|
50
|
+
values = [dict(row.scores) for row in analysis]
|
|
51
|
+
return values[0] if len(values) == 1 else values
|
|
52
|
+
|
|
53
|
+
def get_scores(self, smiles_list):
|
|
54
|
+
"""Return positive and negative scores keyed by SMILES."""
|
|
55
|
+
|
|
56
|
+
analysis = self.analyze_rows(smiles_list, include_features=False)
|
|
57
|
+
molecule_column = self.molecule_columns[0]
|
|
58
|
+
return {
|
|
59
|
+
row.inputs[molecule_column]: dict(row.scores)
|
|
60
|
+
for row in analysis
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def plot_analysis(self, analysis_row, **kwargs):
|
|
64
|
+
"""Plot both score streams from one immutable ISATPN analysis row."""
|
|
65
|
+
|
|
66
|
+
smiles = analysis_row.inputs[self.molecule_columns[0]]
|
|
67
|
+
return self._plot_score(
|
|
68
|
+
smiles,
|
|
69
|
+
{
|
|
70
|
+
"positive": analysis_row.atom_scores("positive"),
|
|
71
|
+
"negative": analysis_row.atom_scores("negative"),
|
|
72
|
+
},
|
|
73
|
+
**kwargs,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def plot_score(self, *args, draw_mol_columns=None, **kwargs):
|
|
77
|
+
"""Calculate and plot both ISATPN score streams."""
|
|
78
|
+
|
|
79
|
+
if draw_mol_columns is None:
|
|
80
|
+
draw_mol_columns = self.molecule_columns[0]
|
|
81
|
+
if draw_mol_columns not in self.molecule_columns:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"{draw_mol_columns} is not a valid molecule column. "
|
|
84
|
+
f"Choose from {self.molecule_columns}."
|
|
85
|
+
)
|
|
86
|
+
inputs, plot_kwargs = self.handle_positional_args(args, kwargs)
|
|
87
|
+
smiles = inputs.get(draw_mol_columns, [None])[0]
|
|
88
|
+
if smiles is None:
|
|
89
|
+
raise ValueError(f"{draw_mol_columns} must be specified.")
|
|
90
|
+
scores = self.get_score(**inputs)
|
|
91
|
+
return self._plot_score(smiles, scores, **plot_kwargs)
|
|
92
|
+
|
|
93
|
+
def get_feature(self, *args, feature=None, **kwargs):
|
|
94
|
+
"""Return row-aligned ISATPN positive/negative hidden features."""
|
|
95
|
+
|
|
96
|
+
analysis = self.analyze_rows(*args, include_features=True, **kwargs)
|
|
97
|
+
rows = []
|
|
98
|
+
for row in analysis:
|
|
99
|
+
values = dict(row.features)
|
|
100
|
+
if feature is not None:
|
|
101
|
+
if feature not in values:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"Feature {feature!r} is unavailable. "
|
|
104
|
+
f"Available features: {list(values)}."
|
|
105
|
+
)
|
|
106
|
+
rows.append(values[feature])
|
|
107
|
+
else:
|
|
108
|
+
rows.append(values)
|
|
109
|
+
return rows[0] if len(rows) == 1 else rows
|
|
110
|
+
|
|
111
|
+
def _plot_score(
|
|
112
|
+
self,
|
|
113
|
+
smiles,
|
|
114
|
+
score,
|
|
115
|
+
*,
|
|
116
|
+
atom_with_index=False,
|
|
117
|
+
score_scaler=lambda value: value,
|
|
118
|
+
ticks=(0, 0.25, 0.5, 0.75, 1),
|
|
119
|
+
tick_type="manual",
|
|
120
|
+
rot=0,
|
|
121
|
+
line_width=2.0,
|
|
122
|
+
locate="right",
|
|
123
|
+
figsize=1,
|
|
124
|
+
only_total=False,
|
|
125
|
+
with_colorbar=True,
|
|
126
|
+
**kwargs,
|
|
127
|
+
):
|
|
128
|
+
"""Plot ISATPN positive, negative, and combined attribution scores."""
|
|
129
|
+
|
|
130
|
+
molecule = Chem.MolFromSmiles(smiles)
|
|
131
|
+
positive = self.get_atom_score(
|
|
132
|
+
smiles,
|
|
133
|
+
np.asarray(score_scaler(score["positive"])),
|
|
134
|
+
)
|
|
135
|
+
negative = self.get_atom_score(
|
|
136
|
+
smiles,
|
|
137
|
+
np.asarray(score_scaler(score["negative"])),
|
|
138
|
+
)
|
|
139
|
+
raw_total = (1 + positive - negative) / 2
|
|
140
|
+
total = np.asarray(score_scaler(raw_total))
|
|
141
|
+
color_map = LinearSegmentedColormap.from_list(
|
|
142
|
+
"isapn_score",
|
|
143
|
+
[(0, "orangered"), (0.5, "white"), (1, "royalblue")],
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def draw(axis, values, title, scale=1):
|
|
147
|
+
png = showAtomHighlight(
|
|
148
|
+
molecule,
|
|
149
|
+
values,
|
|
150
|
+
color_map,
|
|
151
|
+
atom_with_index,
|
|
152
|
+
rot,
|
|
153
|
+
line_width,
|
|
154
|
+
figsize=scale,
|
|
155
|
+
)
|
|
156
|
+
axis.imshow(Image.open(io.BytesIO(png)))
|
|
157
|
+
axis.axis("off")
|
|
158
|
+
axis.set_title(title, fontsize=20)
|
|
159
|
+
|
|
160
|
+
if only_total:
|
|
161
|
+
figure, axis = plt.subplots(
|
|
162
|
+
1,
|
|
163
|
+
1,
|
|
164
|
+
figsize=(16 * figsize, 11 * figsize),
|
|
165
|
+
)
|
|
166
|
+
draw(axis, total, " ", figsize)
|
|
167
|
+
else:
|
|
168
|
+
if locate == "right":
|
|
169
|
+
grid = GridSpec(2, 3)
|
|
170
|
+
positions = (grid[0:2, 0:2], grid[0, 2], grid[1, 2])
|
|
171
|
+
figure = plt.figure(figsize=(16 * figsize, 10 * figsize))
|
|
172
|
+
elif locate == "bottom":
|
|
173
|
+
grid = GridSpec(3, 2)
|
|
174
|
+
positions = (grid[0:2, 0:2], grid[2, 0], grid[2, 1])
|
|
175
|
+
figure = plt.figure(figsize=(12 * figsize, 12 * figsize))
|
|
176
|
+
else:
|
|
177
|
+
raise ValueError("locate must be either 'right' or 'bottom'.")
|
|
178
|
+
axes = [figure.add_subplot(position) for position in positions]
|
|
179
|
+
draw(axes[0], total, " ", figsize)
|
|
180
|
+
draw(axes[1], positive, "Positive", figsize * 0.5)
|
|
181
|
+
draw(axes[2], negative, "Negative", figsize * 0.5)
|
|
182
|
+
axis = axes[0]
|
|
183
|
+
if with_colorbar:
|
|
184
|
+
colorbar = plt.colorbar(
|
|
185
|
+
plt.cm.ScalarMappable(cmap=color_map),
|
|
186
|
+
ax=axis,
|
|
187
|
+
shrink=0.5,
|
|
188
|
+
pad=0,
|
|
189
|
+
)
|
|
190
|
+
colorbar.set_ticks([0, 0.25, 0.5, 0.75, 1])
|
|
191
|
+
labels = ticks
|
|
192
|
+
if tick_type == "minmax":
|
|
193
|
+
labels = np.round(
|
|
194
|
+
np.linspace(np.min(raw_total), np.max(raw_total), 5),
|
|
195
|
+
3,
|
|
196
|
+
)
|
|
197
|
+
colorbar.set_ticklabels(labels)
|
|
198
|
+
plt.show()
|
|
199
|
+
return {
|
|
200
|
+
"positive": positive,
|
|
201
|
+
"negative": negative,
|
|
202
|
+
"total": total,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
def get_subgroup_score_bin(self, smiles_list):
|
|
206
|
+
"""Group both ISATPN score streams by fragment SMILES."""
|
|
207
|
+
|
|
208
|
+
positive_fragments = {}
|
|
209
|
+
negative_fragments = {}
|
|
210
|
+
for smiles, result in self.get_scores(smiles_list).items():
|
|
211
|
+
positive = np.asarray(result["positive"]).reshape(-1, 1)
|
|
212
|
+
negative = np.asarray(result["negative"]).reshape(-1, 1)
|
|
213
|
+
for index, fragment in enumerate(self.get_fragment(smiles)):
|
|
214
|
+
for destination, values in (
|
|
215
|
+
(positive_fragments, positive),
|
|
216
|
+
(negative_fragments, negative),
|
|
217
|
+
):
|
|
218
|
+
current = values[index]
|
|
219
|
+
if fragment.smiles in destination:
|
|
220
|
+
current = np.concatenate(
|
|
221
|
+
[destination[fragment.smiles], current],
|
|
222
|
+
axis=0,
|
|
223
|
+
)
|
|
224
|
+
destination[fragment.smiles] = current
|
|
225
|
+
return positive_fragments, negative_fragments
|
|
226
|
+
|
|
227
|
+
def plot_score_hist(self, smiles_list, xlim=(0, 1), bins=40):
|
|
228
|
+
"""Plot and return both ISATPN score streams."""
|
|
229
|
+
|
|
230
|
+
positive = []
|
|
231
|
+
negative = []
|
|
232
|
+
for result in self.get_scores(smiles_list).values():
|
|
233
|
+
positive.extend(set(np.asarray(result["positive"]).flatten()))
|
|
234
|
+
negative.extend(set(np.asarray(result["negative"]).flatten()))
|
|
235
|
+
edges = np.arange(xlim[0], xlim[1], 1.0 / bins)
|
|
236
|
+
plt.hist(positive, bins=edges, alpha=0.5, label="positive")
|
|
237
|
+
plt.hist(negative, bins=edges, alpha=0.5, label="negative")
|
|
238
|
+
plt.legend()
|
|
239
|
+
plt.xlim(*xlim)
|
|
240
|
+
plt.show()
|
|
241
|
+
return positive, negative
|
|
242
|
+
|
|
243
|
+
def plot_subgroup_score_histogram(
|
|
244
|
+
self,
|
|
245
|
+
smiles_list,
|
|
246
|
+
nums=10,
|
|
247
|
+
bins=40,
|
|
248
|
+
xlim=(0, 1),
|
|
249
|
+
):
|
|
250
|
+
"""Plot fragment histograms separately for both ISATPN streams."""
|
|
251
|
+
|
|
252
|
+
positive, negative = self.get_subgroup_score_bin(smiles_list)
|
|
253
|
+
positive = dict(
|
|
254
|
+
sorted(positive.items(), key=lambda item: len(item[1]), reverse=True)[
|
|
255
|
+
:nums
|
|
256
|
+
]
|
|
257
|
+
)
|
|
258
|
+
negative = dict(
|
|
259
|
+
sorted(negative.items(), key=lambda item: len(item[1]), reverse=True)[
|
|
260
|
+
:nums
|
|
261
|
+
]
|
|
262
|
+
)
|
|
263
|
+
edges = np.arange(xlim[0], xlim[1], 1.0 / bins)
|
|
264
|
+
for title, values in (("Positive", positive), ("Negative", negative)):
|
|
265
|
+
for fragment, scores in values.items():
|
|
266
|
+
plt.hist(scores, bins=edges, alpha=0.5, label=fragment)
|
|
267
|
+
plt.xlim(*xlim)
|
|
268
|
+
plt.legend()
|
|
269
|
+
plt.title(title)
|
|
270
|
+
plt.show()
|
|
271
|
+
return positive, negative
|
|
272
|
+
|
|
273
|
+
def plot_subgroup_score_histogram_byone(
|
|
274
|
+
self,
|
|
275
|
+
smiles_list,
|
|
276
|
+
nums=10,
|
|
277
|
+
bins=40,
|
|
278
|
+
xlim=(0, 1),
|
|
279
|
+
target_subgroups=None,
|
|
280
|
+
):
|
|
281
|
+
"""Plot both ISATPN streams together for each selected fragment."""
|
|
282
|
+
|
|
283
|
+
positive, negative = self.get_subgroup_score_bin(smiles_list)
|
|
284
|
+
positive = dict(
|
|
285
|
+
sorted(positive.items(), key=lambda item: len(item[1]), reverse=True)[
|
|
286
|
+
:nums
|
|
287
|
+
]
|
|
288
|
+
)
|
|
289
|
+
negative = dict(
|
|
290
|
+
sorted(negative.items(), key=lambda item: len(item[1]), reverse=True)[
|
|
291
|
+
:nums
|
|
292
|
+
]
|
|
293
|
+
)
|
|
294
|
+
if target_subgroups:
|
|
295
|
+
positive = {
|
|
296
|
+
key: value
|
|
297
|
+
for key, value in positive.items()
|
|
298
|
+
if key in target_subgroups
|
|
299
|
+
}
|
|
300
|
+
negative = {
|
|
301
|
+
key: value
|
|
302
|
+
for key, value in negative.items()
|
|
303
|
+
if key in target_subgroups
|
|
304
|
+
}
|
|
305
|
+
figure, axes = plt.subplots(
|
|
306
|
+
max(len(positive), 1),
|
|
307
|
+
1,
|
|
308
|
+
figsize=(5, 2 * max(len(positive), 1)),
|
|
309
|
+
squeeze=False,
|
|
310
|
+
)
|
|
311
|
+
edges = np.arange(xlim[0], xlim[1], 1.0 / bins)
|
|
312
|
+
for axis, (fragment, values) in zip(axes[:, 0], positive.items()):
|
|
313
|
+
axis.hist(values, bins=edges, alpha=0.5, label="positive")
|
|
314
|
+
if fragment in negative:
|
|
315
|
+
axis.hist(
|
|
316
|
+
negative[fragment],
|
|
317
|
+
bins=edges,
|
|
318
|
+
alpha=0.5,
|
|
319
|
+
label="negative",
|
|
320
|
+
)
|
|
321
|
+
axis.set_ylabel(fragment)
|
|
322
|
+
axis.set_xlim(*xlim)
|
|
323
|
+
axis.legend()
|
|
324
|
+
plt.show()
|
|
325
|
+
return positive, negative
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class ISAPNAnalyzer(_ISAPNAnalyzerMixin, ISAAnalyzer):
|
|
329
|
+
"""Legacy-compatible Analyzer for ISATPN positive/negative models."""
|
|
330
|
+
|
|
331
|
+
def __init__(self, model_path, save_result=False, **kwargs):
|
|
332
|
+
super().__init__(model_path, save_result, **kwargs)
|
|
333
|
+
self._configure_isapn_interpreter()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class ISAPNAnalyzer_v2(_ISAPNAnalyzerMixin, ISAAnalyzer_v2):
|
|
337
|
+
"""Version 2 Analyzer for ISATPN positive/negative models."""
|
|
338
|
+
|
|
339
|
+
def __init__(self, model_path, save_result=False, **kwargs):
|
|
340
|
+
super().__init__(model_path, save_result, **kwargs)
|
|
341
|
+
self._configure_isapn_interpreter()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class ISAPNAnalyzer_v1p3(ISAPNAnalyzer_v2):
|
|
345
|
+
"""Deprecated compatibility name for :class:`ISAPNAnalyzer_v2`."""
|
|
346
|
+
|
|
347
|
+
def __init__(self, *args, **kwargs):
|
|
348
|
+
warnings.warn(
|
|
349
|
+
"ISAPNAnalyzer_v1p3 is deprecated; use ISAPNAnalyzer_v2 or "
|
|
350
|
+
"Analyzer(...) instead.",
|
|
351
|
+
FutureWarning,
|
|
352
|
+
stacklevel=2,
|
|
353
|
+
)
|
|
354
|
+
super().__init__(*args, **kwargs)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
ISATPNAnalyzer = ISAPNAnalyzer
|
|
358
|
+
ISATPNAnalyzer_v2 = ISAPNAnalyzer_v2
|
|
359
|
+
|
|
360
|
+
__all__ = [
|
|
361
|
+
"ISAPNAnalyzer",
|
|
362
|
+
"ISAPNAnalyzer_v2",
|
|
363
|
+
"ISAPNAnalyzer_v1p3",
|
|
364
|
+
"ISATPNAnalyzer",
|
|
365
|
+
"ISATPNAnalyzer_v2",
|
|
366
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
|
|
2
|
+
import numpy as np
|
|
3
|
+
import torch
|
|
4
|
+
import io
|
|
5
|
+
import rdkit.Chem as Chem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from .ISAAnalyzer import ISAAnalyzer
|
|
9
|
+
|
|
10
|
+
class ISAwSAnalyzer(ISAAnalyzer):
|
|
11
|
+
def check_score_by_group(self):
|
|
12
|
+
if getattr(self.dm.gg,'sculptor',None) is None:
|
|
13
|
+
self.is_score_by_group = True
|
|
14
|
+
else:
|
|
15
|
+
temp_smiles='CC(C)(C)OC(=O)C1=CC=CC=C1C(=O)O'
|
|
16
|
+
temp_solvent = "CS(=O)C"
|
|
17
|
+
|
|
18
|
+
test_loader,_ = self.prepare_temp_data([temp_smiles],solvents=[temp_solvent])
|
|
19
|
+
temp_score = self.tm.get_score(self.nm, test_loader)
|
|
20
|
+
|
|
21
|
+
if 'positive' in temp_score:
|
|
22
|
+
temp_score['positive'] = temp_score['positive'].detach().cpu().numpy()
|
|
23
|
+
if temp_score['positive'].shape[0] == len(self.get_fragment(temp_smiles)):
|
|
24
|
+
self.is_score_by_group = True
|
|
25
|
+
elif temp_score['positive'].shape[0] == Chem.MolFromSmiles(temp_smiles).GetNumAtoms():
|
|
26
|
+
self.is_score_by_group = False
|
|
27
|
+
else:
|
|
28
|
+
raise ValueError('The length of the score and the number of fragments do not match.')
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError('The score is not calculated properly.')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# get the attention score of the given smiles
|
|
34
|
+
def get_score(self, smiles, solvent):
|
|
35
|
+
"get the attention score of the given smiles by its subgroups"
|
|
36
|
+
test_loader,_ = self.prepare_temp_data([smiles],[solvent])
|
|
37
|
+
result = self.tm.get_score(self.nm, test_loader)
|
|
38
|
+
for k in result.keys():
|
|
39
|
+
if type(result[k]) is torch.Tensor:
|
|
40
|
+
result[k] = result[k].detach().cpu().numpy()
|
|
41
|
+
|
|
42
|
+
result['positive'] = self.get_group_score(smiles, result['positive'])
|
|
43
|
+
if 'negative' in result:
|
|
44
|
+
result['negative'] = self.get_group_score(smiles, result['negative'])
|
|
45
|
+
self.save_data(smiles, result)
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
def _get_feature_single_legacy(self, smiles, solvent):
|
|
49
|
+
"""Legacy single-input solvent feature helper."""
|
|
50
|
+
"""
|
|
51
|
+
This function gets the feature of the given smiles and solvent.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
smiles (str): The smiles of the molecule.
|
|
55
|
+
solvent (str): The solvent of the molecule.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
dict: The feature of the given smiles and solvent.
|
|
59
|
+
"""
|
|
60
|
+
test_loader, _ = self.prepare_temp_data([smiles], [solvent])
|
|
61
|
+
return self.tm.get_feature(self.nm, test_loader)
|
|
62
|
+
|
|
63
|
+
def get_feature(self, smiles_list, solvent_list, feature=None):
|
|
64
|
+
results = {}
|
|
65
|
+
|
|
66
|
+
if len(smiles_list) > 0:
|
|
67
|
+
valid_smiles, new_results = self._get_all_features(smiles_list, solvent_list, feature=feature)
|
|
68
|
+
for smiles, result in zip(valid_smiles, new_results):
|
|
69
|
+
results[smiles] = result
|
|
70
|
+
return results
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_all_features(self, smiles_list, solvent_list, feature=None):
|
|
74
|
+
temp_loader,valid_smiles = self.prepare_temp_data(smiles_list, solvent_list)
|
|
75
|
+
features = self.tm.get_feature(self.nm, temp_loader)
|
|
76
|
+
features = {k:features[k].detach().cpu().numpy() for k in features.keys()}
|
|
77
|
+
results = []
|
|
78
|
+
count=0
|
|
79
|
+
for i, smiles in enumerate(valid_smiles['compound']):
|
|
80
|
+
result = {}
|
|
81
|
+
if feature is not None:
|
|
82
|
+
result[feature] = features[feature][i]
|
|
83
|
+
else:
|
|
84
|
+
frag = self.get_fragment(smiles)
|
|
85
|
+
result['feature_P'] = features['feature_P'][count:count+len(frag)]
|
|
86
|
+
if 'feature_N' in features:
|
|
87
|
+
result['feature_N'] = features['feature_N'][count:count+len(frag)]
|
|
88
|
+
count+=len(frag)
|
|
89
|
+
results.append(result)
|
|
90
|
+
self.save_data(smiles, result)
|
|
91
|
+
return valid_smiles['compound'], results
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def plot_score(self, smiles, solvent ,**kwargs):
|
|
96
|
+
"""
|
|
97
|
+
This function plots the attention score of the given smiles by its subgroups.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
smiles (str): The smiles of the molecule.
|
|
101
|
+
atom_with_index (bool): Whether to show the atom index or not.
|
|
102
|
+
score_scaler (function): The function to scale the score. Default is lambda x:x.
|
|
103
|
+
ticks (list): The ticks of the colorbar. Default is [0,0.25,0.5,0.75,1].
|
|
104
|
+
rot (int): The rotation of the molecule. Default is 0.
|
|
105
|
+
locate (str): The location of the subplots. Default is 'right'. It can be 'right' or 'bottom'.
|
|
106
|
+
figsize (float): The size of the figure. Default is 1.
|
|
107
|
+
only_total (bool): Whether to plot only the total score or plot PAS and NAS as well. Default is False.
|
|
108
|
+
with_colorbar (bool): Whether to show the colorbar or not. Default is True.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
dict: The attention score of the given smiles.
|
|
112
|
+
Each value in the dictionary is the attention score of corresponding atom with the same index.
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
score = self.get_score(smiles, solvent)
|
|
116
|
+
return self._plot_score(smiles, score, **kwargs)
|
|
117
|
+
|