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,319 @@
1
+ """Compatibility Analyzer classes backed by the row-preserving inference core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import pickle
8
+ import warnings
9
+ from pathlib import Path
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+ from rdkit import Chem
14
+
15
+ from .core import InferenceCore
16
+ from .results import PredictionResult, UncertaintyResult
17
+
18
+
19
+ class MolAnalyzer:
20
+ """Load a saved model and predict molecular properties.
21
+
22
+ This class preserves the legacy ``predict(smiles, solvent_list=None)`` result
23
+ mapping. New code should use :meth:`predict_rows` when duplicate or invalid
24
+ input rows must be retained.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ model_path,
30
+ save_result=False,
31
+ *,
32
+ device=None,
33
+ batch_size=None,
34
+ model_dir=None,
35
+ ):
36
+ self._core = InferenceCore(
37
+ model_path,
38
+ device=device,
39
+ batch_size=batch_size,
40
+ model_dir=model_dir,
41
+ )
42
+ self.model_path = str(self._core.artifacts.root)
43
+ self.data_path = str(self._core.artifacts.root / "data")
44
+ self.save_result = bool(save_result)
45
+ self.config = self._core.config
46
+ self.dm = self._core.dm
47
+ self.nm = self._core.nm
48
+ self.tm = self._core.tm
49
+ self.scaler = self._core.scaler
50
+ self.molecule_columns = list(self._core.molecule_columns)
51
+ self.numeric_input_columns = list(self._core.numeric_input_columns)
52
+ self.data_keys = ["prediction"]
53
+ self.for_pickle = []
54
+ if self.save_result:
55
+ Path(self.data_path).mkdir(parents=True, exist_ok=True)
56
+
57
+ def predict_rows(self, *args, **kwargs) -> PredictionResult:
58
+ """Predict while retaining every original row and its validation status."""
59
+
60
+ result = self._core.predict(*args, **kwargs)
61
+ self._save_prediction_rows(result)
62
+ return result
63
+
64
+ def prepare_temp_data(self, *args, **kwargs):
65
+ """Compatibility bridge for ISA helpers that operate on prepared loaders."""
66
+
67
+ normalized = self._core.normalize_inputs(args, kwargs)
68
+ self.dm.init_temp_data(**{key: list(value) for key, value in normalized.items()})
69
+ return self.dm.get_Dataloaders(temp=True), {
70
+ **self.dm.valid_smiles,
71
+ **self.dm.numeric_inputs,
72
+ }
73
+
74
+ def predict(self, smiles_list, solvent_list=None, dropout=False):
75
+ """Return the historical SMILES-keyed prediction mapping."""
76
+
77
+ if dropout:
78
+ warnings.warn(
79
+ "dropout=True returns one stochastic draw and is deprecated. "
80
+ "Use predict_uncertainty(samples=..., seed=...) for reproducible MC dropout.",
81
+ DeprecationWarning,
82
+ stacklevel=2,
83
+ )
84
+ inputs = self._legacy_inputs(smiles_list, solvent_list)
85
+ uncertainty = self._core.predict_uncertainty(**inputs, samples=2)
86
+ structured = uncertainty.samples[0]
87
+ else:
88
+ structured = self.predict_rows(**self._legacy_inputs(smiles_list, solvent_list))
89
+
90
+ result = {}
91
+ for row in structured.valid_rows:
92
+ smiles = row.inputs[self.molecule_columns[0]]
93
+ if solvent_list is None:
94
+ result[smiles] = row.prediction
95
+ else:
96
+ solvent_column = self.molecule_columns[1]
97
+ result[(smiles, row.inputs[solvent_column])] = row.prediction
98
+ return result
99
+
100
+ def _legacy_inputs(self, smiles_list, solvent_list):
101
+ inputs = {self.molecule_columns[0]: smiles_list}
102
+ if solvent_list is not None:
103
+ if len(self.molecule_columns) < 2:
104
+ raise ValueError(
105
+ f"Model {self.model_path!r} does not define a solvent molecule column. "
106
+ f"Expected columns: {self.molecule_columns}."
107
+ )
108
+ inputs[self.molecule_columns[1]] = solvent_list
109
+ for column in self.numeric_input_columns:
110
+ if column not in inputs:
111
+ raise ValueError(
112
+ f"Legacy MolAnalyzer.predict cannot infer required numeric input {column!r}. "
113
+ "Use MolAnalyzer_v2.predict with named inputs."
114
+ )
115
+ return inputs
116
+
117
+ def predict_uncertainty(self, *args, samples=30, seed=None, **kwargs) -> UncertaintyResult:
118
+ """Estimate MC-dropout mean/std without changing the default predict output."""
119
+
120
+ return self._core.predict_uncertainty(*args, samples=samples, seed=seed, **kwargs)
121
+
122
+ def predict_csv(
123
+ self,
124
+ input_path,
125
+ output_path=None,
126
+ *,
127
+ index_col=None,
128
+ uncertainty_samples=None,
129
+ uncertainty_seed=None,
130
+ **read_csv_kwargs,
131
+ ):
132
+ """Predict a CSV while preserving every source row and invalid-row error."""
133
+
134
+ source = Path(input_path).expanduser().resolve()
135
+ if not source.is_file():
136
+ raise FileNotFoundError(
137
+ f"Inference CSV {str(source)!r} does not exist. Provide an existing CSV path."
138
+ )
139
+ try:
140
+ frame = pd.read_csv(source, **read_csv_kwargs)
141
+ except (pd.errors.ParserError, pd.errors.EmptyDataError, UnicodeDecodeError) as exc:
142
+ raise ValueError(
143
+ f"Could not read inference CSV {str(source)!r}. "
144
+ "Check its encoding, delimiter, header, and row structure."
145
+ ) from exc
146
+ missing = [column for column in self._core.input_columns if column not in frame.columns]
147
+ if missing:
148
+ raise ValueError(
149
+ f"Inference CSV {str(source)!r} is missing required columns {missing}. "
150
+ f"Available columns: {list(frame.columns)}."
151
+ )
152
+ if index_col is not None and index_col not in frame.columns:
153
+ raise ValueError(
154
+ f"Inference CSV {str(source)!r} does not contain index_col "
155
+ f"{index_col!r}. Available columns: {list(frame.columns)}."
156
+ )
157
+ source_indices = (
158
+ frame[index_col].tolist()
159
+ if index_col is not None
160
+ else frame.index.tolist()
161
+ )
162
+ inputs = {column: frame[column].tolist() for column in self._core.input_columns}
163
+ uncertainty = None
164
+ if uncertainty_samples is None:
165
+ result = self.predict_rows(**inputs)
166
+ else:
167
+ uncertainty = self.predict_uncertainty(
168
+ **inputs,
169
+ samples=uncertainty_samples,
170
+ seed=uncertainty_seed,
171
+ )
172
+ result = uncertainty.mean
173
+ output = result.to_dataframe()
174
+ if uncertainty is not None:
175
+ std_frame = uncertainty.std.to_dataframe()
176
+ for target in self._core.targets:
177
+ output[f"{target}_pred_std"] = std_frame[f"{target}_pred"]
178
+ output["uncertainty_samples"] = uncertainty_samples
179
+ output["row_index"] = source_indices
180
+ destination = (
181
+ Path(output_path).expanduser().resolve()
182
+ if output_path is not None
183
+ else source.with_name(source.stem + "_prediction.csv")
184
+ )
185
+ destination.parent.mkdir(parents=True, exist_ok=True)
186
+ temporary = destination.with_name(destination.name + ".tmp")
187
+ output.to_csv(temporary, index=False)
188
+ temporary.replace(destination)
189
+ return str(destination)
190
+
191
+ def _save_prediction_rows(self, result):
192
+ if not self.save_result:
193
+ return
194
+ for row in result.valid_rows:
195
+ if not self.numeric_input_columns and len(self.molecule_columns) == 1:
196
+ identity = row.inputs[self.molecule_columns[0]]
197
+ elif not self.numeric_input_columns and len(self.molecule_columns) == 2:
198
+ identity = (
199
+ f"{row.inputs[self.molecule_columns[0]]}_"
200
+ f"{row.inputs[self.molecule_columns[1]]}"
201
+ )
202
+ else:
203
+ identity = json.dumps(dict(row.inputs), sort_keys=True, default=str)
204
+ self.save_data(identity, {"prediction": row.prediction})
205
+
206
+ def save_data(self, identity, data):
207
+ """Save compatible per-input Analyzer cache entries."""
208
+
209
+ if not self.save_result:
210
+ return
211
+ for key, value in data.items():
212
+ if key not in self.data_keys:
213
+ raise ValueError(f"Analyzer cache key must be one of {self.data_keys}, got {key!r}.")
214
+ path = Path(self.data_path) / self.get_file_name(identity, key)
215
+ if key in self.for_pickle:
216
+ with path.open("wb") as file:
217
+ pickle.dump(value, file)
218
+ else:
219
+ with path.open("wb") as file:
220
+ np.save(file, np.asarray(value))
221
+
222
+ def load_data(self, identity, key):
223
+ if not self.save_result:
224
+ return None
225
+ path = Path(self.data_path) / self.get_file_name(identity, key)
226
+ if not path.is_file():
227
+ return None
228
+ try:
229
+ with path.open("rb") as file:
230
+ return pickle.load(file) if key in self.for_pickle else np.load(file)
231
+ except (OSError, ValueError, EOFError, pickle.PickleError) as exc:
232
+ warnings.warn(
233
+ f"Ignoring unreadable Analyzer cache file {str(path)!r}: {exc}. "
234
+ "The value will be recalculated.",
235
+ RuntimeWarning,
236
+ stacklevel=2,
237
+ )
238
+ return None
239
+
240
+ def get_file_name(self, identity, key):
241
+ if key not in self.data_keys:
242
+ raise ValueError(f"Analyzer cache key must be one of {self.data_keys}, got {key!r}.")
243
+ digest = hashlib.sha256(str(identity).encode("utf-8")).hexdigest()
244
+ extension = "pickle" if key in self.for_pickle else "np"
245
+ return f"{digest}_{self.data_keys.index(key)}.{extension}"
246
+
247
+
248
+ class MolAnalyzer_v2(MolAnalyzer):
249
+ """Generalized Analyzer for named molecule and numeric input columns."""
250
+
251
+ def __init__(self, model_path, save_result=True, **kwargs):
252
+ super().__init__(model_path, save_result=save_result, **kwargs)
253
+ try:
254
+ version = tuple(int(part) for part in str(self.config.get("version", "1.0")).split("."))
255
+ except ValueError as exc:
256
+ raise ValueError(
257
+ f"Saved model version {self.config.get('version')!r} is not a dotted numeric version."
258
+ ) from exc
259
+ if version < (1, 3):
260
+ raise ValueError(
261
+ "MolAnalyzer_v2 requires config version 2.0 or the compatible "
262
+ "historical version 1.3. Use MolAnalyzer for a version 1.0 model."
263
+ )
264
+
265
+ def predict(self, *args, dropout=False, **kwargs):
266
+ if dropout:
267
+ warnings.warn(
268
+ "dropout=True returns one stochastic draw and is deprecated. "
269
+ "Use predict_uncertainty(samples=..., seed=...) instead.",
270
+ DeprecationWarning,
271
+ stacklevel=2,
272
+ )
273
+ structured = self._core.predict_uncertainty(
274
+ *args, samples=2, **kwargs
275
+ ).samples[0]
276
+ else:
277
+ structured = self.predict_rows(*args, **kwargs)
278
+ return structured.legacy_dict(self._core.input_columns)
279
+
280
+ def handle_positional_args(self, args, kwargs):
281
+ """Compatibility parser used by ISA plotting methods.
282
+
283
+ Analyzer input keys are normalized separately from plotting options, and
284
+ neither the caller's mapping nor its sequence values are mutated.
285
+ """
286
+
287
+ input_kwargs = {}
288
+ other_kwargs = {}
289
+ for key, value in dict(kwargs).items():
290
+ if key in self._core.input_columns:
291
+ input_kwargs[key] = value
292
+ else:
293
+ other_kwargs[key] = value
294
+ normalized = self._core.normalize_inputs(args, input_kwargs)
295
+ return normalized, other_kwargs
296
+
297
+
298
+ class MolAnalyzer_v1p3(MolAnalyzer_v2):
299
+ """Deprecated compatibility name for :class:`MolAnalyzer_v2`."""
300
+
301
+ def __init__(self, *args, **kwargs):
302
+ warnings.warn(
303
+ "MolAnalyzer_v1p3 is deprecated; use MolAnalyzer_v2 or Analyzer(...) instead.",
304
+ FutureWarning,
305
+ stacklevel=2,
306
+ )
307
+ super().__init__(*args, **kwargs)
308
+
309
+
310
+ def mol_with_atom_index(mol):
311
+ """Return an RDKit molecule labelled with its zero-based atom indices."""
312
+
313
+ if isinstance(mol, str):
314
+ mol = Chem.MolFromSmiles(mol)
315
+ if mol is None:
316
+ raise ValueError("Could not parse molecule for atom-index display.")
317
+ for index in range(mol.GetNumAtoms()):
318
+ mol.GetAtomWithIdx(index).SetProp("molAtomMapNumber", str(index))
319
+ return mol
@@ -0,0 +1,54 @@
1
+ """Public Analyzer API.
2
+
3
+ Exports are explicit so adding an internal helper cannot silently change the public API.
4
+ """
5
+
6
+ from .ISAAnalyzer import (
7
+ ISAAnalyzer,
8
+ ISAAnalyzer_v2,
9
+ ISAAnalyzer_v1p3,
10
+ mol_with_atom_index,
11
+ showAtomHighlight,
12
+ )
13
+ from .ISAPNAnalyzer import (
14
+ ISAPNAnalyzer,
15
+ ISAPNAnalyzer_v2,
16
+ ISAPNAnalyzer_v1p3,
17
+ ISATPNAnalyzer,
18
+ ISATPNAnalyzer_v2,
19
+ )
20
+ from .ISAwSAnalyzer import ISAwSAnalyzer
21
+ from .MolAnalyzer import MolAnalyzer, MolAnalyzer_v2, MolAnalyzer_v1p3
22
+ from .core import InferenceCore, ModelArtifacts, predict_ensemble, resolve_model_artifacts
23
+ from .factory import Analyzer, create_analyzer, select_analyzer_class
24
+ from .interpretation import ISAAnalysisResult, ISAAnalysisRow
25
+ from .results import PredictionResult, PredictionRow, UncertaintyResult
26
+
27
+ __all__ = [
28
+ "Analyzer",
29
+ "InferenceCore",
30
+ "ISAAnalyzer",
31
+ "ISAAnalyzer_v2",
32
+ "ISAAnalyzer_v1p3",
33
+ "ISAAnalysisResult",
34
+ "ISAAnalysisRow",
35
+ "ISAPNAnalyzer",
36
+ "ISAPNAnalyzer_v2",
37
+ "ISAPNAnalyzer_v1p3",
38
+ "ISATPNAnalyzer",
39
+ "ISATPNAnalyzer_v2",
40
+ "ISAwSAnalyzer",
41
+ "ModelArtifacts",
42
+ "MolAnalyzer",
43
+ "MolAnalyzer_v2",
44
+ "MolAnalyzer_v1p3",
45
+ "PredictionResult",
46
+ "PredictionRow",
47
+ "UncertaintyResult",
48
+ "create_analyzer",
49
+ "mol_with_atom_index",
50
+ "predict_ensemble",
51
+ "resolve_model_artifacts",
52
+ "select_analyzer_class",
53
+ "showAtomHighlight",
54
+ ]