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,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
+