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,250 @@
|
|
|
1
|
+
"""Molecular split strategies and auditable split reports."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import random
|
|
7
|
+
import uuid
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from rdkit import Chem
|
|
12
|
+
from rdkit.Chem.Scaffolds import MurckoScaffold
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SPLIT_REPORT_SCHEMA_VERSION = 1
|
|
16
|
+
SPLIT_NAMES = ("train", "val", "test")
|
|
17
|
+
DEFAULT_SPLIT_FRACTIONS = (0.8, 0.1, 0.1)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def murcko_scaffold(smiles, include_chirality=False):
|
|
21
|
+
molecule = Chem.MolFromSmiles(str(smiles))
|
|
22
|
+
if molecule is None:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"RDKit could not parse SMILES {smiles!r} while computing a Murcko scaffold. "
|
|
25
|
+
"Fix the molecule or ensure invalid graph rows are filtered before splitting."
|
|
26
|
+
)
|
|
27
|
+
scaffold = MurckoScaffold.GetScaffoldForMol(molecule)
|
|
28
|
+
return Chem.MolToSmiles(
|
|
29
|
+
scaffold,
|
|
30
|
+
canonical=True,
|
|
31
|
+
isomericSmiles=bool(include_chirality),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def scaffold_split_indices(
|
|
36
|
+
smiles,
|
|
37
|
+
seed=42,
|
|
38
|
+
fractions=DEFAULT_SPLIT_FRACTIONS,
|
|
39
|
+
include_chirality=False,
|
|
40
|
+
):
|
|
41
|
+
"""Assign whole Murcko-scaffold groups to reproducible 80/10/10 splits."""
|
|
42
|
+
fractions = tuple(float(value) for value in fractions)
|
|
43
|
+
if len(fractions) != 3 or any(value <= 0 for value in fractions):
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"split fractions must contain three positive values, got {fractions!r}."
|
|
46
|
+
)
|
|
47
|
+
if not np.isclose(sum(fractions), 1.0):
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"split fractions must sum to 1.0, got {fractions!r} "
|
|
50
|
+
f"(sum={sum(fractions)!r})."
|
|
51
|
+
)
|
|
52
|
+
groups = {}
|
|
53
|
+
row_scaffolds = []
|
|
54
|
+
for index, value in enumerate(smiles):
|
|
55
|
+
scaffold = murcko_scaffold(value, include_chirality=include_chirality)
|
|
56
|
+
row_scaffolds.append(scaffold)
|
|
57
|
+
groups.setdefault(scaffold, []).append(index)
|
|
58
|
+
if len(groups) < 3:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Scaffold splitting requires at least 3 distinct Murcko scaffolds, "
|
|
61
|
+
f"but found {len(groups)} across {len(row_scaffolds)} usable rows. "
|
|
62
|
+
"Add structurally diverse molecules or use split_strategy='random' or "
|
|
63
|
+
"a predefined 'set' column."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
rng = random.Random(seed)
|
|
67
|
+
grouped = list(groups.items())
|
|
68
|
+
rng.shuffle(grouped)
|
|
69
|
+
grouped.sort(key=lambda item: -len(item[1]))
|
|
70
|
+
target_counts = np.asarray(fractions) * len(row_scaffolds)
|
|
71
|
+
assigned_groups = {name: [] for name in SPLIT_NAMES}
|
|
72
|
+
assigned_indices = {name: [] for name in SPLIT_NAMES}
|
|
73
|
+
|
|
74
|
+
for scaffold, indices in grouped:
|
|
75
|
+
scores = []
|
|
76
|
+
for position, name in enumerate(SPLIT_NAMES):
|
|
77
|
+
projected = np.asarray(
|
|
78
|
+
[len(assigned_indices[item]) for item in SPLIT_NAMES],
|
|
79
|
+
dtype=float,
|
|
80
|
+
)
|
|
81
|
+
projected[position] += len(indices)
|
|
82
|
+
scores.append(float(np.abs(projected - target_counts).sum()))
|
|
83
|
+
selected = SPLIT_NAMES[min(range(3), key=lambda index: (scores[index], index))]
|
|
84
|
+
assigned_groups[selected].append(scaffold)
|
|
85
|
+
assigned_indices[selected].extend(indices)
|
|
86
|
+
|
|
87
|
+
for empty_name in [
|
|
88
|
+
name for name in SPLIT_NAMES if not assigned_indices[name]
|
|
89
|
+
]:
|
|
90
|
+
donors = [
|
|
91
|
+
name for name in SPLIT_NAMES if len(assigned_groups[name]) > 1
|
|
92
|
+
]
|
|
93
|
+
if not donors:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"Scaffold grouping could not produce non-empty train, val, and test "
|
|
96
|
+
"splits without breaking a scaffold group. Use a more diverse dataset."
|
|
97
|
+
)
|
|
98
|
+
donor = max(donors, key=lambda name: len(assigned_indices[name]))
|
|
99
|
+
scaffold = min(
|
|
100
|
+
assigned_groups[donor],
|
|
101
|
+
key=lambda value: (len(groups[value]), value),
|
|
102
|
+
)
|
|
103
|
+
assigned_groups[donor].remove(scaffold)
|
|
104
|
+
assigned_groups[empty_name].append(scaffold)
|
|
105
|
+
moved = groups[scaffold]
|
|
106
|
+
assigned_indices[donor] = [
|
|
107
|
+
index for index in assigned_indices[donor] if index not in set(moved)
|
|
108
|
+
]
|
|
109
|
+
assigned_indices[empty_name].extend(moved)
|
|
110
|
+
|
|
111
|
+
result = tuple(
|
|
112
|
+
np.asarray(sorted(assigned_indices[name]), dtype=int)
|
|
113
|
+
for name in SPLIT_NAMES
|
|
114
|
+
)
|
|
115
|
+
scaffold_sets = [
|
|
116
|
+
{row_scaffolds[index] for index in indices} for indices in result
|
|
117
|
+
]
|
|
118
|
+
overlap = (
|
|
119
|
+
scaffold_sets[0] & scaffold_sets[1]
|
|
120
|
+
| scaffold_sets[0] & scaffold_sets[2]
|
|
121
|
+
| scaffold_sets[1] & scaffold_sets[2]
|
|
122
|
+
)
|
|
123
|
+
if overlap:
|
|
124
|
+
raise RuntimeError(
|
|
125
|
+
f"Internal scaffold split error: scaffolds crossed split boundaries: "
|
|
126
|
+
f"{sorted(overlap)!r}."
|
|
127
|
+
)
|
|
128
|
+
return result, row_scaffolds
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _target_stats(values):
|
|
132
|
+
array = np.asarray(values, dtype=float)
|
|
133
|
+
result = []
|
|
134
|
+
for column in range(array.shape[1]):
|
|
135
|
+
usable = array[:, column]
|
|
136
|
+
usable = usable[~np.isnan(usable)]
|
|
137
|
+
result.append(
|
|
138
|
+
{
|
|
139
|
+
"count": int(array.shape[0]),
|
|
140
|
+
"usable_count": int(usable.size),
|
|
141
|
+
"missing_count": int(array.shape[0] - usable.size),
|
|
142
|
+
"min": float(usable.min()) if usable.size else None,
|
|
143
|
+
"max": float(usable.max()) if usable.size else None,
|
|
144
|
+
"mean": float(usable.mean()) if usable.size else None,
|
|
145
|
+
"std": float(usable.std(ddof=1)) if usable.size > 1 else None,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def write_split_report(
|
|
152
|
+
config,
|
|
153
|
+
strategy,
|
|
154
|
+
indices_by_split,
|
|
155
|
+
row_indices,
|
|
156
|
+
target_values,
|
|
157
|
+
target_names,
|
|
158
|
+
smiles,
|
|
159
|
+
scaffold_column,
|
|
160
|
+
include_chirality=False,
|
|
161
|
+
):
|
|
162
|
+
"""Write atomic JSON summary and row-level CSV split assignments."""
|
|
163
|
+
scaffolds = [
|
|
164
|
+
murcko_scaffold(value, include_chirality=include_chirality)
|
|
165
|
+
for value in smiles
|
|
166
|
+
]
|
|
167
|
+
scaffold_sets = {
|
|
168
|
+
name: {scaffolds[index] for index in indices}
|
|
169
|
+
for name, indices in zip(SPLIT_NAMES, indices_by_split)
|
|
170
|
+
}
|
|
171
|
+
overlap = sorted(
|
|
172
|
+
scaffold_sets["train"] & scaffold_sets["val"]
|
|
173
|
+
| scaffold_sets["train"] & scaffold_sets["test"]
|
|
174
|
+
| scaffold_sets["val"] & scaffold_sets["test"]
|
|
175
|
+
)
|
|
176
|
+
if strategy == "scaffold" and overlap:
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
f"Scaffold leakage detected after scaffold splitting: {overlap!r}."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
split_target_summary = {}
|
|
182
|
+
for name, indices in zip(SPLIT_NAMES, indices_by_split):
|
|
183
|
+
stats = _target_stats(np.asarray(target_values)[indices])
|
|
184
|
+
split_target_summary[name] = {
|
|
185
|
+
target: value for target, value in zip(target_names, stats)
|
|
186
|
+
}
|
|
187
|
+
report = {
|
|
188
|
+
"split_report_schema_version": SPLIT_REPORT_SCHEMA_VERSION,
|
|
189
|
+
"strategy": strategy,
|
|
190
|
+
"split_random_seed": config.get("split_random_seed", 42),
|
|
191
|
+
"fractions": dict(zip(SPLIT_NAMES, DEFAULT_SPLIT_FRACTIONS)),
|
|
192
|
+
"scaffold_column": scaffold_column,
|
|
193
|
+
"scaffold_include_chirality": bool(include_chirality),
|
|
194
|
+
"counts": {
|
|
195
|
+
name: int(len(indices))
|
|
196
|
+
for name, indices in zip(SPLIT_NAMES, indices_by_split)
|
|
197
|
+
},
|
|
198
|
+
"scaffold_counts": {
|
|
199
|
+
name: len(scaffold_sets[name]) for name in SPLIT_NAMES
|
|
200
|
+
},
|
|
201
|
+
"scaffold_overlap_count": len(overlap),
|
|
202
|
+
"scaffold_overlap": overlap,
|
|
203
|
+
"target_summary": split_target_summary,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
model_path = Path(config["MODEL_PATH"])
|
|
207
|
+
model_path.mkdir(parents=True, exist_ok=True)
|
|
208
|
+
json_path = model_path / "split_report.json"
|
|
209
|
+
csv_path = model_path / "split_assignments.csv"
|
|
210
|
+
token = uuid.uuid4().hex
|
|
211
|
+
json_staging = json_path.with_name(f".{json_path.name}.{token}.tmp")
|
|
212
|
+
csv_staging = csv_path.with_name(f".{csv_path.name}.{token}.tmp")
|
|
213
|
+
try:
|
|
214
|
+
json_staging.write_text(
|
|
215
|
+
json.dumps(report, indent=2, sort_keys=True),
|
|
216
|
+
encoding="utf-8",
|
|
217
|
+
)
|
|
218
|
+
assignment = {}
|
|
219
|
+
for name, indices in zip(SPLIT_NAMES, indices_by_split):
|
|
220
|
+
for index in indices:
|
|
221
|
+
assignment[int(index)] = name
|
|
222
|
+
with open(csv_staging, "w", encoding="utf-8", newline="") as stream:
|
|
223
|
+
writer = csv.DictWriter(
|
|
224
|
+
stream,
|
|
225
|
+
fieldnames=[
|
|
226
|
+
"dataset_index",
|
|
227
|
+
"original_row_index",
|
|
228
|
+
"split",
|
|
229
|
+
"scaffold",
|
|
230
|
+
"smiles",
|
|
231
|
+
],
|
|
232
|
+
)
|
|
233
|
+
writer.writeheader()
|
|
234
|
+
for index in range(len(smiles)):
|
|
235
|
+
writer.writerow(
|
|
236
|
+
{
|
|
237
|
+
"dataset_index": index,
|
|
238
|
+
"original_row_index": int(row_indices[index]),
|
|
239
|
+
"split": assignment[index],
|
|
240
|
+
"scaffold": scaffolds[index],
|
|
241
|
+
"smiles": smiles[index],
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
os.replace(json_staging, json_path)
|
|
245
|
+
os.replace(csv_staging, csv_path)
|
|
246
|
+
finally:
|
|
247
|
+
for staging in (json_staging, csv_staging):
|
|
248
|
+
if staging.exists():
|
|
249
|
+
staging.unlink()
|
|
250
|
+
return report
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import yaml
|
|
3
|
+
import hashlib
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
from . import PATH
|
|
7
|
+
|
|
8
|
+
def save_funtionalgroup_csv(file_path,save_path):
|
|
9
|
+
"""
|
|
10
|
+
save the functional group csv file
|
|
11
|
+
"""
|
|
12
|
+
df = pd.read_csv(file_path)
|
|
13
|
+
df.to_csv(save_path+'/functional_group.csv',index=False)
|
|
14
|
+
|
|
15
|
+
def save_network_refer(file_path,save_path):
|
|
16
|
+
"""
|
|
17
|
+
save the network refer yaml file
|
|
18
|
+
"""
|
|
19
|
+
with open(file_path, "r") as file:
|
|
20
|
+
network_refer = yaml.safe_load(file)
|
|
21
|
+
with open(save_path+'/network_refer.yaml', "w") as file:
|
|
22
|
+
yaml.dump(network_refer, file)
|
|
23
|
+
|
|
24
|
+
def save_network_module(file_path,save_path):
|
|
25
|
+
"""
|
|
26
|
+
save the network module
|
|
27
|
+
"""
|
|
28
|
+
with open(file_path, "r") as file:
|
|
29
|
+
network_module = file.read()
|
|
30
|
+
with open(save_path+'/network.py', "w") as file:
|
|
31
|
+
file.write(network_module)
|
|
32
|
+
|
|
33
|
+
def save_registered_network(network_class, save_path):
|
|
34
|
+
source_path = inspect.getsourcefile(network_class)
|
|
35
|
+
if source_path is None:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"Cannot save network {network_class.__qualname__!r}: its source file "
|
|
38
|
+
"cannot be located. Define custom networks in an importable .py module."
|
|
39
|
+
)
|
|
40
|
+
with open(source_path, "r", encoding="utf-8") as stream:
|
|
41
|
+
source = stream.read()
|
|
42
|
+
class_name = network_class.__qualname__
|
|
43
|
+
if "." in class_name:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Cannot save nested network class {class_name!r}; define it at module scope."
|
|
46
|
+
)
|
|
47
|
+
with open(save_path + '/network.py', "w", encoding="utf-8") as file:
|
|
48
|
+
file.write(source)
|
|
49
|
+
file.write(f"\n\n# Saved D4CMPP2 entry point\nnetwork = {class_name}\n")
|
|
50
|
+
digest = hashlib.sha256(source.encode("utf-8")).hexdigest()
|
|
51
|
+
with open(save_path + "/network_identity.json", "w", encoding="utf-8") as file:
|
|
52
|
+
json.dump(
|
|
53
|
+
{
|
|
54
|
+
"module": network_class.__module__,
|
|
55
|
+
"class": class_name,
|
|
56
|
+
"source_sha256": digest,
|
|
57
|
+
"snapshot": "network.py",
|
|
58
|
+
},
|
|
59
|
+
file,
|
|
60
|
+
indent=2,
|
|
61
|
+
sort_keys=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def save_config(config):
|
|
65
|
+
"""
|
|
66
|
+
save the configuration file
|
|
67
|
+
"""
|
|
68
|
+
with open(config['MODEL_PATH']+'/config.yaml', "w") as file:
|
|
69
|
+
yaml.dump(config, file)
|
|
70
|
+
|
|
71
|
+
def save_additional_files(config):
|
|
72
|
+
"""
|
|
73
|
+
save the additional files
|
|
74
|
+
"""
|
|
75
|
+
if "sculptor_s" in config:
|
|
76
|
+
FRAG_REF = PATH.get_frag_ref_path(config)
|
|
77
|
+
save_funtionalgroup_csv(FRAG_REF,config['MODEL_PATH'])
|
|
78
|
+
|
|
79
|
+
NET_REF = PATH.get_network_refer(config)
|
|
80
|
+
save_network_refer(NET_REF,config['MODEL_PATH'])
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
from D4CMPP2.networks.registry import get_model
|
|
84
|
+
|
|
85
|
+
definition = get_model(config.get("network_id", config["network"]))
|
|
86
|
+
except ValueError:
|
|
87
|
+
definition = None
|
|
88
|
+
if definition is None:
|
|
89
|
+
NET_MODULE = PATH.get_NET_DIR(config)+'/'+config['network']+'.py'
|
|
90
|
+
save_network_module(NET_MODULE,config['MODEL_PATH'])
|
|
91
|
+
else:
|
|
92
|
+
save_registered_network(definition.network, config['MODEL_PATH'])
|
|
93
|
+
|
|
94
|
+
save_config(config)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
|
|
2
|
+
from rdkit.Chem.Draw import rdMolDraw2D
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import rdkit.Chem as Chem
|
|
5
|
+
from rdkit.Chem import Draw
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
import PIL
|
|
9
|
+
import numpy as np
|
|
10
|
+
from PIL import Image
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _figure_to_pil(fig):
|
|
14
|
+
"""Render a Matplotlib figure to an RGB Pillow image."""
|
|
15
|
+
|
|
16
|
+
fig.canvas.draw()
|
|
17
|
+
width, height = fig.canvas.get_width_height()
|
|
18
|
+
rgba = np.asarray(fig.canvas.buffer_rgba())
|
|
19
|
+
return Image.fromarray(rgba.reshape(height, width, 4), mode="RGBA").convert("RGB")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def plot_learning_curve(train_losses,val_losses):
|
|
23
|
+
"Draw the learning curve"
|
|
24
|
+
train_losses = np.array(train_losses)
|
|
25
|
+
val_losses = np.array(val_losses)
|
|
26
|
+
|
|
27
|
+
plt.plot(train_losses, label='Training loss', c= 'k')
|
|
28
|
+
plt.plot(val_losses, label='Validation loss', c= 'r')
|
|
29
|
+
y_max = np.concatenate([train_losses, val_losses]).mean() * 3
|
|
30
|
+
plt.xlabel('Epochs')
|
|
31
|
+
plt.ylabel('Loss')
|
|
32
|
+
plt.ylim(0, y_max)
|
|
33
|
+
plt.legend()
|
|
34
|
+
plt.title('Learning curve')
|
|
35
|
+
|
|
36
|
+
fig = plt.gcf()
|
|
37
|
+
pil_image = _figure_to_pil(fig)
|
|
38
|
+
plt.close()
|
|
39
|
+
return pil_image
|
|
40
|
+
|
|
41
|
+
def plot_prediction(targets, prediction_df):
|
|
42
|
+
"Draw the prediction plot"
|
|
43
|
+
train_df = prediction_df[prediction_df['set']=="train"]
|
|
44
|
+
val_df = prediction_df[prediction_df['set']=="val"]
|
|
45
|
+
test_df = prediction_df[prediction_df['set']=="test"]
|
|
46
|
+
|
|
47
|
+
target_nums = len(targets)
|
|
48
|
+
row_num = 1
|
|
49
|
+
col_num = 1
|
|
50
|
+
while True:
|
|
51
|
+
if row_num*col_num>=target_nums:
|
|
52
|
+
break
|
|
53
|
+
if row_num==col_num:
|
|
54
|
+
row_num+=1
|
|
55
|
+
else:
|
|
56
|
+
col_num+=1
|
|
57
|
+
plt.subplots(row_num,col_num,figsize=(col_num*6,row_num*5))
|
|
58
|
+
for i in range(target_nums):
|
|
59
|
+
plt.subplot(row_num,col_num,i+1)
|
|
60
|
+
plt.scatter(train_df[f"{targets[i]}_true"],train_df[f"{targets[i]}_pred"],label="train",c='k',s=0.5,alpha=0.3)
|
|
61
|
+
plt.scatter(val_df[f"{targets[i]}_true"],val_df[f"{targets[i]}_pred"],label="val",c='r',s=0.5,alpha=0.8)
|
|
62
|
+
plt.scatter(test_df[f"{targets[i]}_true"],test_df[f"{targets[i]}_pred"],label="test",c='b',s=0.5,alpha=0.8)
|
|
63
|
+
plt.xlabel("target")
|
|
64
|
+
plt.ylabel("prediction")
|
|
65
|
+
plt.title(targets[i])
|
|
66
|
+
plt.legend()
|
|
67
|
+
|
|
68
|
+
fig = plt.gcf()
|
|
69
|
+
pil_image = _figure_to_pil(fig)
|
|
70
|
+
plt.close()
|
|
71
|
+
return pil_image
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def mol_with_atom_index( mol ):
|
|
76
|
+
"Return the mol object with atom index"
|
|
77
|
+
if type(mol) is str:
|
|
78
|
+
mol = Chem.MolFromSmiles(mol)
|
|
79
|
+
atoms = mol.GetNumAtoms()
|
|
80
|
+
for idx in range( atoms ):
|
|
81
|
+
mol.GetAtomWithIdx( idx ).SetProp( 'molAtomMapNumber', str( mol.GetAtomWithIdx( idx ).GetIdx() ) )
|
|
82
|
+
return mol
|
|
83
|
+
|
|
84
|
+
def showAtomHighlight(mol,atms_list,atom_with_index=True):
|
|
85
|
+
"""Draw the molecule with atom highlight
|
|
86
|
+
Args:
|
|
87
|
+
mol: str or rdkit.Chem.Mol
|
|
88
|
+
the molecule to draw
|
|
89
|
+
atms_list: list of list of int e.g [[0,1,2],[3,4,5]]
|
|
90
|
+
the list of atoms to highlight. the list of atoms will be drawn with different colors.
|
|
91
|
+
atom_with_index: bool
|
|
92
|
+
if True, the atom index will be shown
|
|
93
|
+
|
|
94
|
+
"""
|
|
95
|
+
if type(mol) is str:
|
|
96
|
+
mol = Chem.MolFromSmiles(mol)
|
|
97
|
+
atom_num = mol.GetNumAtoms()
|
|
98
|
+
if atom_with_index:
|
|
99
|
+
mol=mol_with_atom_index(mol)
|
|
100
|
+
hit_ats=[]
|
|
101
|
+
hit_bonds = []
|
|
102
|
+
hit_ats_colormap={}
|
|
103
|
+
hit_bonds_colormap={}
|
|
104
|
+
|
|
105
|
+
colormap=[(1.0, 0.75, 0.79),
|
|
106
|
+
(1.0, 1.0, 0.6),
|
|
107
|
+
(0.74, 0.99, 0.79),
|
|
108
|
+
(0.68, 0.85, 0.90),
|
|
109
|
+
(0.87, 0.63, 0.87),
|
|
110
|
+
(0.90, 0.90, 0.98),
|
|
111
|
+
(0.82, 0.94, 0.75),
|
|
112
|
+
(0.56, 0.93, 0.56),
|
|
113
|
+
(0.50, 1.0, 0.83),
|
|
114
|
+
(0.80, 0.60, 1.0),
|
|
115
|
+
(1.0, 0.85, 0.73),
|
|
116
|
+
(1.0, 1.0, 0.6),
|
|
117
|
+
(0.53, 0.81, 0.92),
|
|
118
|
+
(1.0, 0.75, 0.79),
|
|
119
|
+
(1.0, 0.94, 0.84)
|
|
120
|
+
]
|
|
121
|
+
for count,atms in enumerate(atms_list):
|
|
122
|
+
atms = [int(x) for x in atms]
|
|
123
|
+
hit_ats=hit_ats+atms
|
|
124
|
+
for i in range(len(atms)):
|
|
125
|
+
hit_ats_colormap[atms[i]]=colormap[count%len(colormap)]
|
|
126
|
+
for j in range(i+1,len(atms)):
|
|
127
|
+
bond=mol.GetBondBetweenAtoms(atms[i],atms[j])
|
|
128
|
+
if bond:
|
|
129
|
+
bond_index=bond.GetIdx()
|
|
130
|
+
hit_bonds.append(bond_index)
|
|
131
|
+
hit_bonds_colormap[bond_index]=colormap[count%len(colormap)]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
d = rdMolDraw2D.MolDraw2DCairo(1000, 800)
|
|
135
|
+
d.DrawMolecule(mol,highlightAtoms = hit_ats, highlightAtomColors=hit_ats_colormap,
|
|
136
|
+
highlightBonds=hit_bonds, highlightBondColors=hit_bonds_colormap)
|
|
137
|
+
d.FinishDrawing()
|
|
138
|
+
d = d.GetDrawingText()
|
|
139
|
+
|
|
140
|
+
img_buf = io.BytesIO(d)
|
|
141
|
+
plot(img_buf)
|
|
142
|
+
|
|
143
|
+
def drawSmiles(smiles):
|
|
144
|
+
img_buf = io.BytesIO()
|
|
145
|
+
img = Draw.MolToImage(Chem.MolFromSmiles(smiles), size=(300,300))
|
|
146
|
+
img.save(img_buf, format='png')
|
|
147
|
+
img_buf.seek(0)
|
|
148
|
+
plot(img_buf)
|
|
149
|
+
|
|
150
|
+
def plot(buf):
|
|
151
|
+
image = PIL.Image.open(buf)
|
|
152
|
+
|
|
153
|
+
plt.imshow(image)
|
|
154
|
+
plt.axis('off')
|
|
155
|
+
plt.show()
|
|
156
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Transfer-learning state selection and audit reporting."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import uuid
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
TRANSFER_REPORT_SCHEMA_VERSION = 1
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _tensor_description(tensor):
|
|
16
|
+
return {
|
|
17
|
+
"shape": list(tensor.shape),
|
|
18
|
+
"dtype": str(tensor.dtype),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def select_compatible_state(source_state, target_state):
|
|
23
|
+
"""Return name/shape-compatible source state and a complete audit report."""
|
|
24
|
+
if not isinstance(source_state, dict):
|
|
25
|
+
raise TypeError(
|
|
26
|
+
"Transfer checkpoint must contain a model state-dict mapping parameter "
|
|
27
|
+
f"names to tensors, got {type(source_state).__name__}."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
non_tensors = [
|
|
31
|
+
name for name, value in source_state.items() if not isinstance(value, torch.Tensor)
|
|
32
|
+
]
|
|
33
|
+
if non_tensors:
|
|
34
|
+
raise TypeError(
|
|
35
|
+
"Transfer checkpoint contains non-tensor state entries "
|
|
36
|
+
f"{non_tensors[:10]!r}. Check that final.pth is a plain model state dict."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
selected = {}
|
|
40
|
+
loaded = []
|
|
41
|
+
shape_mismatch = []
|
|
42
|
+
source_only = []
|
|
43
|
+
for name, source_value in source_state.items():
|
|
44
|
+
target_value = target_state.get(name)
|
|
45
|
+
if target_value is None:
|
|
46
|
+
source_only.append({"name": name, **_tensor_description(source_value)})
|
|
47
|
+
elif source_value.shape != target_value.shape:
|
|
48
|
+
shape_mismatch.append(
|
|
49
|
+
{
|
|
50
|
+
"name": name,
|
|
51
|
+
"source_shape": list(source_value.shape),
|
|
52
|
+
"target_shape": list(target_value.shape),
|
|
53
|
+
"source_dtype": str(source_value.dtype),
|
|
54
|
+
"target_dtype": str(target_value.dtype),
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
selected[name] = source_value
|
|
59
|
+
loaded.append(
|
|
60
|
+
{
|
|
61
|
+
"name": name,
|
|
62
|
+
"shape": list(source_value.shape),
|
|
63
|
+
"source_dtype": str(source_value.dtype),
|
|
64
|
+
"target_dtype": str(target_value.dtype),
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
target_only = [
|
|
69
|
+
{"name": name, **_tensor_description(value)}
|
|
70
|
+
for name, value in target_state.items()
|
|
71
|
+
if name not in source_state
|
|
72
|
+
]
|
|
73
|
+
report = {
|
|
74
|
+
"transfer_report_schema_version": TRANSFER_REPORT_SCHEMA_VERSION,
|
|
75
|
+
"counts": {
|
|
76
|
+
"loaded": len(loaded),
|
|
77
|
+
"shape_mismatch": len(shape_mismatch),
|
|
78
|
+
"source_only": len(source_only),
|
|
79
|
+
"target_only": len(target_only),
|
|
80
|
+
},
|
|
81
|
+
"loaded": loaded,
|
|
82
|
+
"shape_mismatch": shape_mismatch,
|
|
83
|
+
"source_only": source_only,
|
|
84
|
+
"target_only": target_only,
|
|
85
|
+
}
|
|
86
|
+
return selected, report
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def file_sha256(path):
|
|
90
|
+
digest = hashlib.sha256()
|
|
91
|
+
with open(path, "rb") as stream:
|
|
92
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
93
|
+
digest.update(chunk)
|
|
94
|
+
return digest.hexdigest()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def write_transfer_report(report, model_path):
|
|
98
|
+
"""Atomically write the additive transfer audit artifact."""
|
|
99
|
+
path = Path(model_path) / "transfer_report.json"
|
|
100
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
staging = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
|
|
102
|
+
try:
|
|
103
|
+
staging.write_text(
|
|
104
|
+
json.dumps(report, indent=2, sort_keys=True),
|
|
105
|
+
encoding="utf-8",
|
|
106
|
+
)
|
|
107
|
+
os.replace(staging, path)
|
|
108
|
+
finally:
|
|
109
|
+
if staging.exists():
|
|
110
|
+
staging.unlink()
|
|
111
|
+
return str(path)
|