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.
Files changed (103) hide show
  1. D4CMPP2/_Data/AGENTS.md +24 -0
  2. D4CMPP2/_Data/Aqsoldb.csv +9291 -0
  3. D4CMPP2/_Data/BradleyMP.csv +3042 -0
  4. D4CMPP2/_Data/Lipophilicity.csv +1131 -0
  5. D4CMPP2/_Data/README.md +8 -0
  6. D4CMPP2/_Data/__init__.py +26 -0
  7. D4CMPP2/_Data/optical.csv +20237 -0
  8. D4CMPP2/_Data/test.csv +190 -0
  9. D4CMPP2/__init__.py +16 -0
  10. D4CMPP2/__main__.py +5 -0
  11. D4CMPP2/_main.py +500 -0
  12. D4CMPP2/cli.py +7 -0
  13. D4CMPP2/exceptions.py +53 -0
  14. D4CMPP2/grid_search.py +259 -0
  15. D4CMPP2/network_refer.yaml +160 -0
  16. D4CMPP2/networks/AFP_model.py +72 -0
  17. D4CMPP2/networks/AFPwithSolv_model.py +72 -0
  18. D4CMPP2/networks/DMPNN_model.py +90 -0
  19. D4CMPP2/networks/DMPNNwithSolv_model.py +89 -0
  20. D4CMPP2/networks/GAT_model.py +48 -0
  21. D4CMPP2/networks/GATwithSolv_model.py +63 -0
  22. D4CMPP2/networks/GCN_model.py +113 -0
  23. D4CMPP2/networks/GCNwithSolv_model.py +103 -0
  24. D4CMPP2/networks/GC_model.py +122 -0
  25. D4CMPP2/networks/ISATPM_model.py +14 -0
  26. D4CMPP2/networks/ISATPN_model.py +199 -0
  27. D4CMPP2/networks/ISAT_model.py +90 -0
  28. D4CMPP2/networks/MPNN_model.py +56 -0
  29. D4CMPP2/networks/MPNNwithSolv_model.py +72 -0
  30. D4CMPP2/networks/__init__.py +25 -0
  31. D4CMPP2/networks/base.py +250 -0
  32. D4CMPP2/networks/registry.py +187 -0
  33. D4CMPP2/networks/src/AFP.py +118 -0
  34. D4CMPP2/networks/src/BiDropout.py +29 -0
  35. D4CMPP2/networks/src/DMPNN.py +35 -0
  36. D4CMPP2/networks/src/GAT.py +69 -0
  37. D4CMPP2/networks/src/GC.py +85 -0
  38. D4CMPP2/networks/src/GCN.py +71 -0
  39. D4CMPP2/networks/src/ISAT.py +153 -0
  40. D4CMPP2/networks/src/Linear.py +49 -0
  41. D4CMPP2/networks/src/MPNN.py +56 -0
  42. D4CMPP2/networks/src/SolventLayer.py +62 -0
  43. D4CMPP2/networks/src/__init__.py +0 -0
  44. D4CMPP2/networks/src/distGCN.py +21 -0
  45. D4CMPP2/networks/src/pyg_hetero.py +24 -0
  46. D4CMPP2/optimize.py +472 -0
  47. D4CMPP2/src/Analyzer/ISAAnalyzer.py +458 -0
  48. D4CMPP2/src/Analyzer/ISAPNAnalyzer.py +366 -0
  49. D4CMPP2/src/Analyzer/ISAwSAnalyzer.py +117 -0
  50. D4CMPP2/src/Analyzer/MolAnalyzer.py +319 -0
  51. D4CMPP2/src/Analyzer/__init__.py +54 -0
  52. D4CMPP2/src/Analyzer/core.py +480 -0
  53. D4CMPP2/src/Analyzer/factory.py +166 -0
  54. D4CMPP2/src/Analyzer/interpretation.py +232 -0
  55. D4CMPP2/src/Analyzer/results.py +101 -0
  56. D4CMPP2/src/DataManager/Dataset/GraphDataset.py +314 -0
  57. D4CMPP2/src/DataManager/Dataset/ISAGraphDataset.py +384 -0
  58. D4CMPP2/src/DataManager/Dataset/__init__.py +0 -0
  59. D4CMPP2/src/DataManager/GraphGenerator/ISAGraphGenerator.py +223 -0
  60. D4CMPP2/src/DataManager/GraphGenerator/MolGraphGenerator.py +73 -0
  61. D4CMPP2/src/DataManager/GraphGenerator/__init__.py +14 -0
  62. D4CMPP2/src/DataManager/ISADataManager.py +67 -0
  63. D4CMPP2/src/DataManager/MolDataManager.py +735 -0
  64. D4CMPP2/src/DataManager/__init__.py +14 -0
  65. D4CMPP2/src/DataManager/contracts.py +179 -0
  66. D4CMPP2/src/NetworkManager/ISANetworkManager.py +12 -0
  67. D4CMPP2/src/NetworkManager/NetworkManager.py +520 -0
  68. D4CMPP2/src/NetworkManager/__init__.py +14 -0
  69. D4CMPP2/src/PostProcessor.py +160 -0
  70. D4CMPP2/src/TrainManager/ISATrainManager.py +26 -0
  71. D4CMPP2/src/TrainManager/TrainManager.py +254 -0
  72. D4CMPP2/src/TrainManager/__init__.py +14 -0
  73. D4CMPP2/src/TrainManager/callbacks.py +119 -0
  74. D4CMPP2/src/__init__.py +0 -0
  75. D4CMPP2/src/utils/PATH.py +246 -0
  76. D4CMPP2/src/utils/__init__.py +0 -0
  77. D4CMPP2/src/utils/argparser.py +56 -0
  78. D4CMPP2/src/utils/checkpointing.py +90 -0
  79. D4CMPP2/src/utils/config_resolution.py +123 -0
  80. D4CMPP2/src/utils/config_validation.py +370 -0
  81. D4CMPP2/src/utils/csv_validation.py +105 -0
  82. D4CMPP2/src/utils/data_quality.py +181 -0
  83. D4CMPP2/src/utils/featureizer.py +202 -0
  84. D4CMPP2/src/utils/functional_group.csv +169 -0
  85. D4CMPP2/src/utils/graph_cache.py +213 -0
  86. D4CMPP2/src/utils/leaderboard.py +212 -0
  87. D4CMPP2/src/utils/metrics.py +31 -0
  88. D4CMPP2/src/utils/module_loader.py +147 -0
  89. D4CMPP2/src/utils/output.py +80 -0
  90. D4CMPP2/src/utils/reproducibility.py +70 -0
  91. D4CMPP2/src/utils/run_manifest.py +175 -0
  92. D4CMPP2/src/utils/scaler.py +40 -0
  93. D4CMPP2/src/utils/sculptor.py +713 -0
  94. D4CMPP2/src/utils/splitting.py +250 -0
  95. D4CMPP2/src/utils/supportfile_saver.py +94 -0
  96. D4CMPP2/src/utils/tools.py +156 -0
  97. D4CMPP2/src/utils/transfer_learning.py +111 -0
  98. d4cmpp2-0.4.0.dist-info/METADATA +420 -0
  99. d4cmpp2-0.4.0.dist-info/RECORD +103 -0
  100. d4cmpp2-0.4.0.dist-info/WHEEL +5 -0
  101. d4cmpp2-0.4.0.dist-info/entry_points.txt +2 -0
  102. d4cmpp2-0.4.0.dist-info/licenses/LICENSE +21 -0
  103. 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)