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,232 @@
|
|
|
1
|
+
"""Row- and index-aligned ISA interpretation results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Mapping, Sequence
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import torch
|
|
10
|
+
from rdkit import Chem
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ISAAnalysisRow:
|
|
15
|
+
"""Scores and features aligned to one molecule's atoms or fragments."""
|
|
16
|
+
|
|
17
|
+
row_index: int
|
|
18
|
+
inputs: Mapping[str, Any]
|
|
19
|
+
atom_count: int
|
|
20
|
+
fragment_atom_indices: tuple[tuple[int, ...], ...]
|
|
21
|
+
scores: Mapping[str, np.ndarray]
|
|
22
|
+
score_mode: str
|
|
23
|
+
features: Mapping[str, np.ndarray]
|
|
24
|
+
feature_mode: str | None
|
|
25
|
+
prediction: np.ndarray | None = None
|
|
26
|
+
|
|
27
|
+
def atom_scores(self, key="positive") -> np.ndarray:
|
|
28
|
+
"""Return a score for every atom, expanding fragment scores if needed."""
|
|
29
|
+
|
|
30
|
+
if key not in self.scores:
|
|
31
|
+
raise KeyError(f"Score {key!r} is unavailable. Available scores: {list(self.scores)}.")
|
|
32
|
+
values = np.asarray(self.scores[key])
|
|
33
|
+
if self.score_mode == "atom":
|
|
34
|
+
if values.shape[0] != self.atom_count:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"Atom score {key!r} has {values.shape[0]} rows, expected {self.atom_count}."
|
|
37
|
+
)
|
|
38
|
+
return values
|
|
39
|
+
output_shape = (self.atom_count,) + values.shape[1:]
|
|
40
|
+
expanded = np.zeros(output_shape, dtype=values.dtype)
|
|
41
|
+
for fragment_index, atoms in enumerate(self.fragment_atom_indices):
|
|
42
|
+
expanded[list(atoms)] = values[fragment_index]
|
|
43
|
+
return expanded
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class ISAAnalysisResult(Sequence[ISAAnalysisRow]):
|
|
48
|
+
"""Valid ISA analysis rows in original input order."""
|
|
49
|
+
|
|
50
|
+
rows: tuple[ISAAnalysisRow, ...]
|
|
51
|
+
invalid_rows: tuple[Any, ...] = ()
|
|
52
|
+
|
|
53
|
+
def __len__(self):
|
|
54
|
+
return len(self.rows)
|
|
55
|
+
|
|
56
|
+
def __getitem__(self, index):
|
|
57
|
+
return self.rows[index]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ISAInterpreter:
|
|
61
|
+
"""Execute score/feature extraction and validate all index boundaries."""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
core,
|
|
66
|
+
*,
|
|
67
|
+
score_keys=("positive",),
|
|
68
|
+
feature_keys=(),
|
|
69
|
+
score_mode=None,
|
|
70
|
+
):
|
|
71
|
+
self.core = core
|
|
72
|
+
self.score_keys = tuple(score_keys)
|
|
73
|
+
self.feature_keys = tuple(feature_keys)
|
|
74
|
+
self.score_mode = score_mode
|
|
75
|
+
sculptor = getattr(getattr(core.dm, "gg", None), "sculptor", None)
|
|
76
|
+
if sculptor is None:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Model {str(core.artifacts.root)!r} does not expose the ISA sculptor "
|
|
79
|
+
"required for fragment-aligned interpretation."
|
|
80
|
+
)
|
|
81
|
+
self.sculptor = sculptor
|
|
82
|
+
|
|
83
|
+
def _fragments(self, smiles):
|
|
84
|
+
molecule = Chem.MolFromSmiles(smiles)
|
|
85
|
+
if molecule is None:
|
|
86
|
+
raise ValueError(f"Could not parse SMILES {smiles!r} for ISA fragmentation.")
|
|
87
|
+
fragments = self.sculptor.fragmentation_with_condition(
|
|
88
|
+
molecule, draw=False, get_index=False
|
|
89
|
+
)
|
|
90
|
+
indices = tuple(tuple(int(index) for index in fragment.atoms) for fragment in fragments)
|
|
91
|
+
flattened = sorted(index for fragment in indices for index in fragment)
|
|
92
|
+
expected = list(range(molecule.GetNumAtoms()))
|
|
93
|
+
if flattened != expected:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"ISA fragments for {smiles!r} do not cover each atom exactly once. "
|
|
96
|
+
f"Observed atom indices: {flattened}; expected: {expected}."
|
|
97
|
+
)
|
|
98
|
+
return molecule.GetNumAtoms(), indices
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _numpy_mapping(value, kind):
|
|
102
|
+
if not isinstance(value, dict):
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"ISA {kind} extraction must return a dictionary, got {type(value).__name__}. "
|
|
105
|
+
"This saved network does not implement the requested interpretation contract."
|
|
106
|
+
)
|
|
107
|
+
return {
|
|
108
|
+
key: item.detach().cpu().numpy() if isinstance(item, torch.Tensor) else np.asarray(item)
|
|
109
|
+
for key, item in value.items()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _split(mapping, counts, *, skip=()):
|
|
114
|
+
total = sum(counts)
|
|
115
|
+
result = [dict() for _ in counts]
|
|
116
|
+
for key, values in mapping.items():
|
|
117
|
+
if key in skip:
|
|
118
|
+
continue
|
|
119
|
+
array = np.asarray(values)
|
|
120
|
+
if array.shape[0] != total:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
f"ISA output {key!r} has leading dimension {array.shape[0]}, "
|
|
123
|
+
f"but aligned rows require {total} ({counts})."
|
|
124
|
+
)
|
|
125
|
+
start = 0
|
|
126
|
+
for row, count in zip(result, counts):
|
|
127
|
+
row[key] = array[start:start + count]
|
|
128
|
+
start += count
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
def analyze(self, *args, include_features=True, **kwargs):
|
|
132
|
+
normalized = self.core.normalize_inputs(args, kwargs)
|
|
133
|
+
loader, valid_indices = self.core._prepare(normalized)
|
|
134
|
+
if not valid_indices:
|
|
135
|
+
prediction = self.core._result_from_scores(
|
|
136
|
+
normalized, valid_indices, np.empty((0, len(self.core.targets)))
|
|
137
|
+
)
|
|
138
|
+
return ISAAnalysisResult((), prediction.invalid_rows)
|
|
139
|
+
|
|
140
|
+
draw_column = self.core.molecule_columns[0]
|
|
141
|
+
fragment_rows = []
|
|
142
|
+
for row_index in valid_indices:
|
|
143
|
+
fragment_rows.append(self._fragments(normalized[draw_column][row_index]))
|
|
144
|
+
atom_counts = [item[0] for item in fragment_rows]
|
|
145
|
+
fragment_counts = [len(item[1]) for item in fragment_rows]
|
|
146
|
+
|
|
147
|
+
score_output = self._numpy_mapping(
|
|
148
|
+
self.core.tm.get_score(self.core.nm, loader), "score"
|
|
149
|
+
)
|
|
150
|
+
prediction_values = score_output.pop("prediction", None)
|
|
151
|
+
network_name = str(self.core.config.get("network", ""))
|
|
152
|
+
missing_scores = [key for key in self.score_keys if key not in score_output]
|
|
153
|
+
if missing_scores:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Saved network {network_name!r} did not return required ISA scores "
|
|
156
|
+
f"{missing_scores!r}. Returned keys: {list(score_output)!r}."
|
|
157
|
+
)
|
|
158
|
+
score_output = {key: score_output[key] for key in self.score_keys}
|
|
159
|
+
score_mode = self.score_mode or (
|
|
160
|
+
"fragment"
|
|
161
|
+
if network_name in {"GC_model", "ISATPN_model", "ISATPM_model"}
|
|
162
|
+
else "atom"
|
|
163
|
+
)
|
|
164
|
+
score_counts = fragment_counts if score_mode == "fragment" else atom_counts
|
|
165
|
+
score_rows = self._split(score_output, score_counts)
|
|
166
|
+
|
|
167
|
+
feature_rows = [dict() for _ in valid_indices]
|
|
168
|
+
feature_mode = None
|
|
169
|
+
if include_features:
|
|
170
|
+
if not self.feature_keys:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"Saved network {network_name!r} does not provide ISATPN hidden features. "
|
|
173
|
+
"Call analyze_rows(..., include_features=False) for score-only analysis."
|
|
174
|
+
)
|
|
175
|
+
feature_output = self._numpy_mapping(
|
|
176
|
+
self.core.tm.get_feature(self.core.nm, loader), "feature"
|
|
177
|
+
)
|
|
178
|
+
# ISATPN's get_feature branch returns dot/group-node hidden states.
|
|
179
|
+
missing_features = [
|
|
180
|
+
key for key in self.feature_keys if key not in feature_output
|
|
181
|
+
]
|
|
182
|
+
if missing_features:
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"Saved network {network_name!r} did not return required ISATPN "
|
|
185
|
+
f"features {missing_features!r}. Returned keys: "
|
|
186
|
+
f"{list(feature_output)!r}."
|
|
187
|
+
)
|
|
188
|
+
feature_values = {
|
|
189
|
+
key: feature_output[key] for key in self.feature_keys
|
|
190
|
+
}
|
|
191
|
+
feature_rows = self._split(feature_values, fragment_counts)
|
|
192
|
+
feature_mode = "fragment"
|
|
193
|
+
|
|
194
|
+
if prediction_values is not None:
|
|
195
|
+
prediction_values = self.core._inverse_transform(prediction_values)
|
|
196
|
+
if prediction_values.shape[0] != len(valid_indices):
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f"ISA prediction output has {prediction_values.shape[0]} rows, "
|
|
199
|
+
f"expected {len(valid_indices)}."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
rows = []
|
|
203
|
+
for position, row_index in enumerate(valid_indices):
|
|
204
|
+
rows.append(
|
|
205
|
+
ISAAnalysisRow(
|
|
206
|
+
row_index=row_index,
|
|
207
|
+
inputs={
|
|
208
|
+
column: normalized[column][row_index]
|
|
209
|
+
for column in self.core.input_columns
|
|
210
|
+
},
|
|
211
|
+
atom_count=atom_counts[position],
|
|
212
|
+
fragment_atom_indices=fragment_rows[position][1],
|
|
213
|
+
scores=score_rows[position],
|
|
214
|
+
score_mode=score_mode,
|
|
215
|
+
features=feature_rows[position],
|
|
216
|
+
feature_mode=feature_mode,
|
|
217
|
+
prediction=(
|
|
218
|
+
None if prediction_values is None
|
|
219
|
+
else np.asarray(prediction_values[position])
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
prediction_result = self.core._result_from_scores(
|
|
224
|
+
normalized,
|
|
225
|
+
valid_indices,
|
|
226
|
+
(
|
|
227
|
+
prediction_values
|
|
228
|
+
if prediction_values is not None
|
|
229
|
+
else np.zeros((len(valid_indices), len(self.core.targets)))
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
return ISAAnalysisResult(tuple(rows), prediction_result.invalid_rows)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Structured, row-preserving results for model inference."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Iterable, Iterator, Mapping, Sequence
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class PredictionRow:
|
|
14
|
+
"""One input row and its prediction status."""
|
|
15
|
+
|
|
16
|
+
row_index: Any
|
|
17
|
+
inputs: Mapping[str, Any]
|
|
18
|
+
prediction: np.ndarray | None = None
|
|
19
|
+
status: str = "ok"
|
|
20
|
+
error: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class PredictionResult(Sequence[PredictionRow]):
|
|
25
|
+
"""Prediction rows in the same order as the caller's input."""
|
|
26
|
+
|
|
27
|
+
rows: tuple[PredictionRow, ...]
|
|
28
|
+
targets: tuple[str, ...]
|
|
29
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
def __len__(self) -> int:
|
|
32
|
+
return len(self.rows)
|
|
33
|
+
|
|
34
|
+
def __getitem__(self, index):
|
|
35
|
+
return self.rows[index]
|
|
36
|
+
|
|
37
|
+
def __iter__(self) -> Iterator[PredictionRow]:
|
|
38
|
+
return iter(self.rows)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def valid_rows(self) -> tuple[PredictionRow, ...]:
|
|
42
|
+
return tuple(row for row in self.rows if row.status == "ok")
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def invalid_rows(self) -> tuple[PredictionRow, ...]:
|
|
46
|
+
return tuple(row for row in self.rows if row.status != "ok")
|
|
47
|
+
|
|
48
|
+
def legacy_dict(self, input_columns: Iterable[str]) -> dict[tuple[Any, ...], np.ndarray]:
|
|
49
|
+
"""Return the historical tuple-keyed mapping.
|
|
50
|
+
|
|
51
|
+
Duplicate input tuples cannot be represented by the historical mapping. Callers
|
|
52
|
+
that need every row should use this structured result or ``to_dataframe()``.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
columns = tuple(input_columns)
|
|
56
|
+
result = {}
|
|
57
|
+
for row in self.valid_rows:
|
|
58
|
+
result[tuple(row.inputs[column] for column in columns)] = row.prediction
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
def to_dataframe(self):
|
|
62
|
+
"""Convert all rows, including invalid rows, to a pandas DataFrame."""
|
|
63
|
+
|
|
64
|
+
import pandas as pd
|
|
65
|
+
|
|
66
|
+
records = []
|
|
67
|
+
for row in self.rows:
|
|
68
|
+
record = {"row_index": row.row_index, **dict(row.inputs)}
|
|
69
|
+
if row.prediction is None:
|
|
70
|
+
for target in self.targets:
|
|
71
|
+
record[f"{target}_pred"] = np.nan
|
|
72
|
+
else:
|
|
73
|
+
values = np.asarray(row.prediction).reshape(-1)
|
|
74
|
+
for index, target in enumerate(self.targets):
|
|
75
|
+
record[f"{target}_pred"] = values[index]
|
|
76
|
+
record["prediction_status"] = row.status
|
|
77
|
+
record["prediction_error"] = row.error
|
|
78
|
+
records.append(record)
|
|
79
|
+
return pd.DataFrame.from_records(records)
|
|
80
|
+
|
|
81
|
+
def to_csv(self, path, **kwargs) -> str:
|
|
82
|
+
"""Write all rows atomically and return the resolved output path."""
|
|
83
|
+
|
|
84
|
+
destination = Path(path).expanduser().resolve()
|
|
85
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
temporary = destination.with_name(destination.name + ".tmp")
|
|
87
|
+
self.to_dataframe().to_csv(temporary, index=False, **kwargs)
|
|
88
|
+
temporary.replace(destination)
|
|
89
|
+
return str(destination)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class UncertaintyResult:
|
|
94
|
+
"""Repeated stochastic predictions for one row-preserving input."""
|
|
95
|
+
|
|
96
|
+
mean: PredictionResult
|
|
97
|
+
std: PredictionResult
|
|
98
|
+
samples: tuple[PredictionResult, ...]
|
|
99
|
+
method: str
|
|
100
|
+
seed: int | None
|
|
101
|
+
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch.utils.data import Dataset
|
|
3
|
+
from torch_geometric.data import Batch
|
|
4
|
+
import numpy as np
|
|
5
|
+
from ..contracts import (
|
|
6
|
+
GENERAL_BATCH_CONTRACT,
|
|
7
|
+
LEGACY_GENERAL_BATCH_CONTRACT,
|
|
8
|
+
LEGACY_SOLVENT_BATCH_CONTRACT,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
class GraphDataset_legacy(Dataset):
|
|
12
|
+
batch_contract = LEGACY_GENERAL_BATCH_CONTRACT
|
|
13
|
+
def __init__(self, graphs=None, target=None, smiles=None):
|
|
14
|
+
if graphs is None: return
|
|
15
|
+
self.graphs = graphs
|
|
16
|
+
self.node_feature = [g.x for g in graphs]
|
|
17
|
+
self.edge_feature = [g.edge_attr for g in graphs]
|
|
18
|
+
self.target = torch.tensor(target).float()
|
|
19
|
+
self.smiles = smiles
|
|
20
|
+
|
|
21
|
+
def __len__(self):
|
|
22
|
+
return len(self.graphs)
|
|
23
|
+
|
|
24
|
+
def __getitem__(self, idx):
|
|
25
|
+
return self.graphs[idx], self.node_feature[idx], self.edge_feature[idx], self.target[idx], self.smiles[idx]
|
|
26
|
+
|
|
27
|
+
# reload the dataset with new data
|
|
28
|
+
def reload(self, data):
|
|
29
|
+
self.graphs, self.node_feature, self.edge_feature, self.target, self.smiles = data
|
|
30
|
+
|
|
31
|
+
self.target= torch.stack(self.target,dim=0)
|
|
32
|
+
if self.target.dim() == 1:
|
|
33
|
+
self.target = self.target.unsqueeze(-1)
|
|
34
|
+
|
|
35
|
+
# get a subset of the dataset with given indices
|
|
36
|
+
def get_subDataset(self, idx):
|
|
37
|
+
graphs = [self.graphs[i] for i in idx]
|
|
38
|
+
node_feature = [self.node_feature[i] for i in idx]
|
|
39
|
+
edge_feature = [self.edge_feature[i] for i in idx]
|
|
40
|
+
target = [self.target[i] for i in idx]
|
|
41
|
+
smiles = [self.smiles[i] for i in idx]
|
|
42
|
+
|
|
43
|
+
dataset = GraphDataset_legacy()
|
|
44
|
+
dataset.reload((graphs, node_feature, edge_feature, target, smiles))
|
|
45
|
+
return dataset
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def collate(samples):
|
|
50
|
+
graphs, node_feature, edge_feature, target, smiles = map(list, zip(*samples))
|
|
51
|
+
batched_graph = Batch.from_data_list(graphs)
|
|
52
|
+
return batched_graph, torch.concat(node_feature,dim=0), torch.concat(edge_feature,dim=0), torch.stack(target,dim=0), smiles
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def unwrapper(batch_graph, node_feature, edge_feature, target, smiles,device='cpu'):
|
|
56
|
+
batch_graph = batch_graph.to(device=device)
|
|
57
|
+
node_feature = node_feature.float().to(device=device)
|
|
58
|
+
edge_feature = edge_feature.float().to(device=device)
|
|
59
|
+
target = target.float().to(device=device)
|
|
60
|
+
return {"graph":batch_graph, "node_feats":node_feature, "edge_feats":edge_feature, "target":target, "smiles":smiles}
|
|
61
|
+
|
|
62
|
+
class GraphDataset_withSolv(GraphDataset_legacy):
|
|
63
|
+
batch_contract = LEGACY_SOLVENT_BATCH_CONTRACT
|
|
64
|
+
def __init__(self, graphs=None, solv_graphs=None, target=None, smiles=None, solv_smiles=None):
|
|
65
|
+
super().__init__(graphs, target, smiles)
|
|
66
|
+
if graphs is None: return
|
|
67
|
+
self.solv_graphs = solv_graphs
|
|
68
|
+
self.solv_node_feature = [g.x for g in solv_graphs]
|
|
69
|
+
self.solv_edge_feature = [g.edge_attr for g in solv_graphs]
|
|
70
|
+
self.solv_smiles = solv_smiles
|
|
71
|
+
|
|
72
|
+
def __getitem__(self, idx):
|
|
73
|
+
args = super().__getitem__(idx)
|
|
74
|
+
return args + (self.solv_graphs[idx], self.solv_node_feature[idx], self.solv_edge_feature[idx], self.solv_smiles[idx])
|
|
75
|
+
|
|
76
|
+
def reload(self, data):
|
|
77
|
+
self.graphs, self.node_feature, self.edge_feature, self.target, self.smiles, self.solv_graphs, self.solv_node_feature, self.solv_edge_feature, self.solv_smiles = data
|
|
78
|
+
|
|
79
|
+
self.target= torch.stack(self.target,dim=0)
|
|
80
|
+
if self.target.dim() == 1:
|
|
81
|
+
self.target = self.target.unsqueeze(-1)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# def subDataset(self, idx):
|
|
85
|
+
# super().subDataset(idx)
|
|
86
|
+
# self.solv_graphs = [self.solv_graphs[i] for i in idx]
|
|
87
|
+
# self.solv_node_feature = [self.solv_node_feature[i] for i in idx]
|
|
88
|
+
# self.solv_edge_feature = [self.solv_edge_feature[i] for i in idx]
|
|
89
|
+
# self.solv_smiles = [self.solv_smiles[i] for i in idx]
|
|
90
|
+
|
|
91
|
+
def get_subDataset(self, idx):
|
|
92
|
+
graphs = [self.graphs[i] for i in idx]
|
|
93
|
+
node_feature = [self.node_feature[i] for i in idx]
|
|
94
|
+
edge_feature = [self.edge_feature[i] for i in idx]
|
|
95
|
+
target = [self.target[i] for i in idx]
|
|
96
|
+
smiles = [self.smiles[i] for i in idx]
|
|
97
|
+
|
|
98
|
+
solv_graphs = [self.solv_graphs[i] for i in idx]
|
|
99
|
+
solv_node_feature = [self.solv_node_feature[i] for i in idx]
|
|
100
|
+
solv_edge_feature = [self.solv_edge_feature[i] for i in idx]
|
|
101
|
+
solv_smiles = [self.solv_smiles[i] for i in idx]
|
|
102
|
+
|
|
103
|
+
dataset = GraphDataset_withSolv()
|
|
104
|
+
dataset.reload((graphs, node_feature, edge_feature, target, smiles, solv_graphs, solv_node_feature, solv_edge_feature, solv_smiles))
|
|
105
|
+
return dataset
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def collate(samples):
|
|
110
|
+
graphs, node_feature, edge_feature, target, smiles, solv_graphs, solv_node_feature, solv_edge_feature, solv_smiles = map(list, zip(*samples))
|
|
111
|
+
|
|
112
|
+
batched_graph = Batch.from_data_list(graphs)
|
|
113
|
+
batched_solv_graph = Batch.from_data_list(solv_graphs)
|
|
114
|
+
return batched_graph, torch.concat(node_feature,dim=0), torch.concat(edge_feature,dim=0), batched_solv_graph, torch.concat(solv_node_feature,dim=0), torch.concat(solv_edge_feature,dim=0), torch.stack(target,dim=0), smiles, solv_smiles
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def unwrapper(batch_graph, node_feature, edge_feature, batch_solv_graph, solv_node_feature, solv_edge_feature, target, smiles, solv_smiles, device='cpu'):
|
|
118
|
+
batch_graph = batch_graph.to(device=device)
|
|
119
|
+
node_feature = node_feature.float().to(device=device)
|
|
120
|
+
edge_feature = edge_feature.float().to(device=device)
|
|
121
|
+
batch_solv_graph = batch_solv_graph.to(device=device)
|
|
122
|
+
solv_node_feature = solv_node_feature.float().to(device=device)
|
|
123
|
+
solv_edge_feature = solv_edge_feature.float().to(device=device)
|
|
124
|
+
target = target.float().to(device=device)
|
|
125
|
+
|
|
126
|
+
return {"graph":batch_graph, "node_feats":node_feature, "edge_feats":edge_feature, "solv_graph":batch_solv_graph, "solv_node_feats":solv_node_feature, "solv_edge_feats":solv_edge_feature, "target":target, "smiles":smiles, "solv_smiles":solv_smiles}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class GraphDataset(GraphDataset_legacy):
|
|
130
|
+
batch_contract = GENERAL_BATCH_CONTRACT
|
|
131
|
+
def __init__(self, graphs : dict = None, numeric_inputs : dict = None, target : list = None, smiles : dict = None, row_indices=None):
|
|
132
|
+
if graphs is None:
|
|
133
|
+
self.graphs = {}
|
|
134
|
+
self.target = None
|
|
135
|
+
self.smiles = {}
|
|
136
|
+
self.data_keys = []
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
for key in graphs:
|
|
140
|
+
setattr(self, key + '_graphs', graphs[key])
|
|
141
|
+
setattr(self, key + '_node_feature', [g.x for g in graphs[key]])
|
|
142
|
+
setattr(self, key + '_edge_feature', [g.edge_attr for g in graphs[key]])
|
|
143
|
+
|
|
144
|
+
if numeric_inputs is not None:
|
|
145
|
+
for key in numeric_inputs:
|
|
146
|
+
setattr(self, key + '_var', numeric_inputs[key])
|
|
147
|
+
else:
|
|
148
|
+
numeric_inputs = {}
|
|
149
|
+
|
|
150
|
+
for key in smiles:
|
|
151
|
+
if key not in graphs:
|
|
152
|
+
raise ValueError(f"Key '{key}' in smiles is not found in graphs.")
|
|
153
|
+
setattr(self, key + '_smiles', smiles[key])
|
|
154
|
+
|
|
155
|
+
self.target = torch.as_tensor(target).float() if target is not None else None
|
|
156
|
+
self.original_row_index = (
|
|
157
|
+
torch.as_tensor(row_indices, dtype=torch.long) if row_indices is not None else None
|
|
158
|
+
)
|
|
159
|
+
self.data_keys = list(graphs.keys()) + list(numeric_inputs.keys()) + ['target']
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def __len__(self):
|
|
163
|
+
if getattr(self, 'target', None) is not None:
|
|
164
|
+
return len(self.target)
|
|
165
|
+
else:
|
|
166
|
+
if len(self.data_keys) == 0:
|
|
167
|
+
return 0
|
|
168
|
+
key = self.data_keys[0]
|
|
169
|
+
if hasattr(self, key + '_graphs'):
|
|
170
|
+
return len(getattr(self, key + '_graphs'))
|
|
171
|
+
elif hasattr(self, key + '_var'):
|
|
172
|
+
return len(getattr(self, key + '_var'))
|
|
173
|
+
elif hasattr(self, key + '_smiles'):
|
|
174
|
+
return len(getattr(self, key + '_smiles'))
|
|
175
|
+
else:
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def __getitem__(self, idx):
|
|
180
|
+
item = {}
|
|
181
|
+
for key in self.data_keys:
|
|
182
|
+
if hasattr(self, key + '_graphs'):
|
|
183
|
+
item[key + '_graphs'] = getattr(self, key + '_graphs')[idx]
|
|
184
|
+
if hasattr(self, key + '_node_feature'):
|
|
185
|
+
nf = getattr(self, key + '_node_feature')[idx]
|
|
186
|
+
if type(nf) is not torch.Tensor:
|
|
187
|
+
nf = torch.tensor(nf, dtype=torch.float32)
|
|
188
|
+
item[key + '_node_feature'] = nf
|
|
189
|
+
if hasattr(self, key + '_edge_feature'):
|
|
190
|
+
ef = getattr(self, key + '_edge_feature')[idx]
|
|
191
|
+
if type(ef) is not torch.Tensor:
|
|
192
|
+
ef = torch.tensor(ef, dtype=torch.float32)
|
|
193
|
+
item[key + '_edge_feature'] = ef
|
|
194
|
+
if hasattr(self, key + '_smiles'):
|
|
195
|
+
item[key + '_smiles'] = getattr(self, key + '_smiles')[idx]
|
|
196
|
+
elif hasattr(self, key + '_var'):
|
|
197
|
+
var = getattr(self, key + '_var')[idx]
|
|
198
|
+
if torch.is_tensor(var):
|
|
199
|
+
var = var.float()
|
|
200
|
+
else:
|
|
201
|
+
var = torch.tensor(var, dtype=torch.float32)
|
|
202
|
+
item[key + '_var'] = var
|
|
203
|
+
elif hasattr(self, key + '_smiles'):
|
|
204
|
+
item[key + '_smiles'] = getattr(self, key + '_smiles')[idx]
|
|
205
|
+
if hasattr(self, 'target') and self.target is not None:
|
|
206
|
+
item['target'] = self.target[idx]
|
|
207
|
+
if self.original_row_index is not None:
|
|
208
|
+
item['original_row_index'] = self.original_row_index[idx]
|
|
209
|
+
return item
|
|
210
|
+
|
|
211
|
+
def reload(self, data):
|
|
212
|
+
if len(data) == 0:
|
|
213
|
+
self.target = None
|
|
214
|
+
self.data_keys = []
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
new_data_keys = []
|
|
218
|
+
for key in data[0]:
|
|
219
|
+
values = [d[key] for d in data]
|
|
220
|
+
if key == 'target':
|
|
221
|
+
if len(values) == 0:
|
|
222
|
+
setattr(self, key, None)
|
|
223
|
+
elif torch.is_tensor(values[0]):
|
|
224
|
+
target = torch.stack(values, dim=0).float()
|
|
225
|
+
if target.dim() == 1:
|
|
226
|
+
target = target.unsqueeze(-1)
|
|
227
|
+
setattr(self, key, target)
|
|
228
|
+
else:
|
|
229
|
+
target = torch.tensor(values, dtype=torch.float32)
|
|
230
|
+
if target.dim() == 1:
|
|
231
|
+
target = target.unsqueeze(-1)
|
|
232
|
+
setattr(self, key, target)
|
|
233
|
+
elif key == 'original_row_index':
|
|
234
|
+
setattr(self, key, torch.as_tensor(values, dtype=torch.long))
|
|
235
|
+
elif key.endswith('_var'):
|
|
236
|
+
if len(values) == 0:
|
|
237
|
+
setattr(self, key, torch.empty((0,), dtype=torch.float32))
|
|
238
|
+
elif torch.is_tensor(values[0]):
|
|
239
|
+
setattr(self, key, torch.stack(values, dim=0).float())
|
|
240
|
+
else:
|
|
241
|
+
setattr(self, key, torch.tensor(values, dtype=torch.float32))
|
|
242
|
+
else:
|
|
243
|
+
setattr(self, key, values)
|
|
244
|
+
if key.endswith('_graphs'):
|
|
245
|
+
new_data_keys.append(key[:-7]) # Remove '_graphs'
|
|
246
|
+
elif key.endswith('_var'):
|
|
247
|
+
new_data_keys.append(key[:-4]) # Remove '_var'
|
|
248
|
+
elif key == 'target':
|
|
249
|
+
new_data_keys.append(key)
|
|
250
|
+
self.data_keys = new_data_keys
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def subDataset(self, idx):
|
|
254
|
+
for key in self.data_keys:
|
|
255
|
+
if hasattr(self, key + '_graphs'):
|
|
256
|
+
setattr(self, key + '_graphs', [getattr(self, key + '_graphs')[i] for i in idx])
|
|
257
|
+
setattr(self, key + '_node_feature', [getattr(self, key + '_node_feature')[i] for i in idx])
|
|
258
|
+
setattr(self, key + '_edge_feature', [getattr(self, key + '_edge_feature')[i] for i in idx])
|
|
259
|
+
elif hasattr(self, key + '_var'):
|
|
260
|
+
var = getattr(self, key + '_var')
|
|
261
|
+
if torch.is_tensor(var):
|
|
262
|
+
setattr(self, key + '_var', var[torch.tensor(idx, dtype=torch.long)])
|
|
263
|
+
else:
|
|
264
|
+
setattr(self, key + '_var', var[np.array(idx, dtype=int)])
|
|
265
|
+
elif hasattr(self, key + '_smiles'):
|
|
266
|
+
setattr(self, key + '_smiles', [getattr(self, key + '_smiles')[i] for i in idx])
|
|
267
|
+
if self.target is not None:
|
|
268
|
+
self.target = self.target[np.array(idx, dtype=int)]
|
|
269
|
+
if self.original_row_index is not None:
|
|
270
|
+
self.original_row_index = self.original_row_index[torch.as_tensor(idx, dtype=torch.long)]
|
|
271
|
+
|
|
272
|
+
def get_subDataset(self, idx):
|
|
273
|
+
dataset = GraphDataset()
|
|
274
|
+
dataset.reload([self[i] for i in idx])
|
|
275
|
+
return dataset
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def collate(samples):
|
|
280
|
+
batched_data = {}
|
|
281
|
+
for key in samples[0].keys():
|
|
282
|
+
if key.endswith('_graphs'):
|
|
283
|
+
batched_data[key] = Batch.from_data_list([s[key] for s in samples])
|
|
284
|
+
elif key.endswith('_node_feature'):
|
|
285
|
+
batched_data[key] = torch.concat([s[key] for s in samples], dim=0)
|
|
286
|
+
elif key.endswith('_edge_feature'):
|
|
287
|
+
batched_data[key] = torch.concat([s[key] for s in samples], dim=0)
|
|
288
|
+
elif key.endswith('_var'):
|
|
289
|
+
batched_data[key] = torch.stack([s[key] for s in samples], dim=0)
|
|
290
|
+
elif key.endswith('_smiles'):
|
|
291
|
+
batched_data[key] = [s[key] for s in samples]
|
|
292
|
+
elif key == 'target':
|
|
293
|
+
batched_data[key] = torch.stack([s[key].reshape(-1) for s in samples], dim=0)
|
|
294
|
+
elif key == 'original_row_index':
|
|
295
|
+
batched_data[key] = torch.stack([s[key].reshape(()) for s in samples], dim=0)
|
|
296
|
+
return batched_data
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def unwrapper(device='cpu', **batched_data):
|
|
300
|
+
for key in batched_data.keys():
|
|
301
|
+
if key.endswith('_graphs'):
|
|
302
|
+
batched_data[key] = batched_data[key].to(device=device)
|
|
303
|
+
elif key.endswith('_node_feature') or key.endswith('_edge_feature'):
|
|
304
|
+
batched_data[key] = batched_data[key].float().to(device=device)
|
|
305
|
+
elif key.endswith('_var'):
|
|
306
|
+
batched_data[key] = batched_data[key].float().to(device=device)
|
|
307
|
+
elif key == 'target':
|
|
308
|
+
batched_data[key] = batched_data[key].float().to(device=device)
|
|
309
|
+
elif key == 'original_row_index':
|
|
310
|
+
batched_data[key] = batched_data[key].long().to(device=device)
|
|
311
|
+
return batched_data
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|