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,735 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
from rdkit import Chem
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
from torch.utils.data import Dataset, DataLoader
|
|
8
|
+
from sklearn.model_selection import train_test_split
|
|
9
|
+
|
|
10
|
+
from .GraphGenerator.MolGraphGenerator import MolGraphGenerator
|
|
11
|
+
from .Dataset.GraphDataset import GraphDataset, GraphDataset_withSolv
|
|
12
|
+
|
|
13
|
+
from D4CMPP2.src.utils.scaler import Scaler
|
|
14
|
+
from D4CMPP2.src.utils.csv_validation import (
|
|
15
|
+
validate_aligned_lengths,
|
|
16
|
+
validate_automatic_split_size,
|
|
17
|
+
validate_csv_schema,
|
|
18
|
+
validate_nonempty_targets,
|
|
19
|
+
validate_numeric_columns,
|
|
20
|
+
validate_set_labels,
|
|
21
|
+
)
|
|
22
|
+
from D4CMPP2.src.utils.data_quality import write_data_quality_report
|
|
23
|
+
from D4CMPP2.src.utils.graph_cache import (
|
|
24
|
+
atomic_save_graph_cache,
|
|
25
|
+
build_graph_recipe,
|
|
26
|
+
cache_path,
|
|
27
|
+
legacy_cache_paths,
|
|
28
|
+
validate_graph,
|
|
29
|
+
validate_payload,
|
|
30
|
+
)
|
|
31
|
+
from D4CMPP2.src.utils.reproducibility import seed_data_loader_worker
|
|
32
|
+
from D4CMPP2.src.utils.output import get_output
|
|
33
|
+
from D4CMPP2.src.utils.splitting import (
|
|
34
|
+
scaffold_split_indices,
|
|
35
|
+
write_split_report,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
import hashlib
|
|
39
|
+
import warnings
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MolDataManager:
|
|
43
|
+
"""The class for the management of the molecular data, preparing the dataset, and dataloader
|
|
44
|
+
|
|
45
|
+
This class is intended to provide the dataset and dataloader for the training, validation, and test.
|
|
46
|
+
This class works mainly in two steps.
|
|
47
|
+
|
|
48
|
+
1. call the init_data() to load the data from the csv file and prepare the graph.
|
|
49
|
+
1-1. the load_data() will load the data from the csv file, saving the smiles and target values.
|
|
50
|
+
1-2. the prepare_graph() will prepare the graph from the saved smiles, trying to load the graph first, then generate the graph if it does not exist.
|
|
51
|
+
|
|
52
|
+
2. call the get_Dataloaders() to provide the dataloader for the training, validation, and test.
|
|
53
|
+
2-1. the prepare_dataset() will drop the invalid data and initialize the dataset with generated graphs and target values.
|
|
54
|
+
2-2. the split_data() will split the dataset into train, val, and test. if the set is given, the dataset will be split according to the set.
|
|
55
|
+
2-3. the init_Dataloader() will initialize the dataloader based on the set and partial_train.
|
|
56
|
+
"""
|
|
57
|
+
feature_dimension_keys = ("node_dim", "edge_dim")
|
|
58
|
+
|
|
59
|
+
def __init__(self, config):
|
|
60
|
+
self.config = config
|
|
61
|
+
self.output = get_output(config)
|
|
62
|
+
self.data = config["data"]
|
|
63
|
+
self.target = config["target"]
|
|
64
|
+
self.scaler = Scaler(config.get("scaler","identity"))
|
|
65
|
+
self.explicit_h_columns = config.get("explicit_h_columns",[])
|
|
66
|
+
self.molecule_columns = config.get("molecule_columns", ['compound'])
|
|
67
|
+
self.numeric_input_columns = config.get("numeric_input_columns", [])
|
|
68
|
+
self.molecule_graphs={col:[] for col in self.molecule_columns}
|
|
69
|
+
self._molecule_smiles = {}
|
|
70
|
+
self.molecule_smiles = {}
|
|
71
|
+
self.valid_smiles = {}
|
|
72
|
+
self.original_row_indices = np.array([], dtype=int)
|
|
73
|
+
self.graph_errors = []
|
|
74
|
+
self.random_seed = config.get("split_random_seed",42)
|
|
75
|
+
training_seed = config.get("random_seed")
|
|
76
|
+
self._partial_train_rng = (
|
|
77
|
+
np.random.default_rng(training_seed)
|
|
78
|
+
if training_seed is not None else None
|
|
79
|
+
)
|
|
80
|
+
self.set = None
|
|
81
|
+
self.import_others()
|
|
82
|
+
config.update({"node_dim":self.gg.node_dim, "edge_dim":self.gg.edge_dim})
|
|
83
|
+
|
|
84
|
+
def _output(self):
|
|
85
|
+
"""Return the configured adapter, including for legacy test fixtures."""
|
|
86
|
+
|
|
87
|
+
return getattr(self, "output", get_output(getattr(self, "config", None)))
|
|
88
|
+
|
|
89
|
+
# Import the graph generator and dataset
|
|
90
|
+
def import_others(self):
|
|
91
|
+
self.graph_type = 'mol'
|
|
92
|
+
self.gg =MolGraphGenerator()
|
|
93
|
+
self.dataset =GraphDataset
|
|
94
|
+
self.unwrapper = self.dataset.unwrapper
|
|
95
|
+
|
|
96
|
+
# Initialize the temporary data for the prediction
|
|
97
|
+
def init_temp_data(self, *args, **kwargs):
|
|
98
|
+
if len(args)+len(kwargs) != len(self.molecule_columns) + len(self.numeric_input_columns):
|
|
99
|
+
raise ValueError(f"Please provide the smiles for {self.molecule_columns} and numeric input for {self.numeric_input_columns}.")
|
|
100
|
+
if len(args) > 0:
|
|
101
|
+
self._output().info(
|
|
102
|
+
"[Data] Positional inference inputs follow this order: "
|
|
103
|
+
f"molecule columns={self.molecule_columns!r}, "
|
|
104
|
+
f"numeric columns={self.numeric_input_columns!r}."
|
|
105
|
+
)
|
|
106
|
+
if len(args) == len(self.molecule_columns) + len(self.numeric_input_columns):
|
|
107
|
+
kwargs = {self.molecule_columns[i]: args[i] for i in range(len(self.molecule_columns))}
|
|
108
|
+
kwargs.update({self.numeric_input_columns[i]: args[i + len(self.molecule_columns)] for i in range(len(self.numeric_input_columns))})
|
|
109
|
+
elif len(args) == len(self.molecule_columns):
|
|
110
|
+
kwargs.update({self.molecule_columns[i]: args[i] for i in range(len(self.molecule_columns))})
|
|
111
|
+
elif len(args) == len(self.numeric_input_columns):
|
|
112
|
+
kwargs.update({self.numeric_input_columns[i]: args[i] for i in range(len(self.numeric_input_columns))})
|
|
113
|
+
else:
|
|
114
|
+
raise ValueError(f"Please provide the smiles for {self.molecule_columns} and numeric input for {self.numeric_input_columns}.")
|
|
115
|
+
if len(kwargs) >0:
|
|
116
|
+
for k in kwargs.keys():
|
|
117
|
+
if k not in self.molecule_columns and k not in self.numeric_input_columns:
|
|
118
|
+
raise ValueError(f"Unknown key {k}. Please provide the smiles for {self.molecule_columns} or numeric input for {self.numeric_input_columns}.")
|
|
119
|
+
for k in self.molecule_columns:
|
|
120
|
+
if k not in kwargs:
|
|
121
|
+
raise ValueError(f"Please provide the smiles for {k}.")
|
|
122
|
+
for k in self.numeric_input_columns:
|
|
123
|
+
if k not in kwargs:
|
|
124
|
+
raise ValueError(f"Please provide the numeric input for {k}.")
|
|
125
|
+
for k in kwargs.keys():
|
|
126
|
+
if type(kwargs[k]) is not list :
|
|
127
|
+
kwargs[k] = [kwargs[k]]
|
|
128
|
+
inputs = {col: kwargs[col] for col in self.molecule_columns if col in kwargs}
|
|
129
|
+
|
|
130
|
+
self._molecule_smiles = {col: np.array(inputs[col]) for col in self.molecule_columns}
|
|
131
|
+
self.molecule_smiles = {col: arr.copy() for col, arr in self._molecule_smiles.items()}
|
|
132
|
+
self.valid_smiles = {col: np.array(inputs[col]) for col in self.molecule_columns}
|
|
133
|
+
self.numeric_inputs = {col: np.array(kwargs[col]) for col in self.numeric_input_columns if col in kwargs}
|
|
134
|
+
self.original_row_indices = np.arange(len(self._molecule_smiles[self.molecule_columns[0]]))
|
|
135
|
+
|
|
136
|
+
self.molecule_graphs={col:[] for col in self.molecule_columns}
|
|
137
|
+
self.target_value = np.zeros((len(self._molecule_smiles[self.molecule_columns[0]]), self.config["target_dim"]))
|
|
138
|
+
self.gg.verbose = False
|
|
139
|
+
for col in self.molecule_columns:
|
|
140
|
+
self.generate_graph(col)
|
|
141
|
+
|
|
142
|
+
# Keep all arrays aligned by applying one centralized mask after graph generation.
|
|
143
|
+
self.drop_none_graph()
|
|
144
|
+
|
|
145
|
+
result = self.valid_smiles.copy()
|
|
146
|
+
result.update(self.numeric_inputs)
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
# (main function) Initialize the data from the csv file based on the configuration
|
|
150
|
+
def init_data(self):
|
|
151
|
+
self.graph_errors = []
|
|
152
|
+
self.load_data()
|
|
153
|
+
self.prepare_graph()
|
|
154
|
+
self.write_graph_error_report()
|
|
155
|
+
self.write_data_quality_report()
|
|
156
|
+
|
|
157
|
+
def load_csv(self):
|
|
158
|
+
path = os.path.join(self.config['DATA_PATH'])
|
|
159
|
+
encodings = ('utf-8', 'cp949', 'euc-kr')
|
|
160
|
+
decode_errors = []
|
|
161
|
+
for encoding in encodings:
|
|
162
|
+
try:
|
|
163
|
+
self.df = pd.read_csv(path, encoding=encoding)
|
|
164
|
+
return
|
|
165
|
+
except UnicodeDecodeError as exc:
|
|
166
|
+
decode_errors.append(f"{encoding}: {exc}")
|
|
167
|
+
except pd.errors.EmptyDataError as exc:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"CSV file {path!r} is empty or has no columns. "
|
|
170
|
+
"Add a header row and data rows before training."
|
|
171
|
+
) from exc
|
|
172
|
+
except pd.errors.ParserError as exc:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"Could not parse CSV file {path!r} using encoding {encoding!r}: {exc}. "
|
|
175
|
+
"Check delimiters, quoting, and malformed rows."
|
|
176
|
+
) from exc
|
|
177
|
+
except OSError as exc:
|
|
178
|
+
raise OSError(
|
|
179
|
+
f"Could not read CSV file {path!r} using encoding {encoding!r}: {exc}. "
|
|
180
|
+
"Check that the path exists and is readable."
|
|
181
|
+
) from exc
|
|
182
|
+
raise UnicodeError(
|
|
183
|
+
f"Could not decode CSV file {path!r}. Tried encodings {list(encodings)}. "
|
|
184
|
+
f"Decode errors: {decode_errors}. Save the file as UTF-8 or specify a supported Korean encoding."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Load the data from the csv file, saving the smiles and target values
|
|
188
|
+
def load_data(self):
|
|
189
|
+
self.load_csv()
|
|
190
|
+
self.write_data_quality_report()
|
|
191
|
+
path = self.config['DATA_PATH']
|
|
192
|
+
validate_csv_schema(
|
|
193
|
+
path,
|
|
194
|
+
self.df.columns,
|
|
195
|
+
len(self.df),
|
|
196
|
+
self.molecule_columns,
|
|
197
|
+
self.target,
|
|
198
|
+
self.numeric_input_columns,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
converted_targets = self.df[self.target].apply(pd.to_numeric, errors='coerce')
|
|
202
|
+
invalid_targets = {
|
|
203
|
+
col: self.df.index[(converted_targets[col].isna()) & (self.df[col].notna())].tolist()
|
|
204
|
+
for col in self.target
|
|
205
|
+
}
|
|
206
|
+
validate_numeric_columns(path, invalid_targets, "Target")
|
|
207
|
+
validate_nonempty_targets(
|
|
208
|
+
path,
|
|
209
|
+
{col: bool(converted_targets[col].isna().all()) for col in self.target},
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if self.numeric_input_columns:
|
|
213
|
+
converted_numeric = self.df[self.numeric_input_columns].apply(pd.to_numeric, errors='coerce')
|
|
214
|
+
invalid_numeric = {
|
|
215
|
+
col: self.df.index[(converted_numeric[col].isna()) & (self.df[col].notna())].tolist()
|
|
216
|
+
for col in self.numeric_input_columns
|
|
217
|
+
}
|
|
218
|
+
validate_numeric_columns(path, invalid_numeric, "Numeric input")
|
|
219
|
+
|
|
220
|
+
if "set" in self.df.columns:
|
|
221
|
+
validate_set_labels(path, self.df["set"].tolist())
|
|
222
|
+
else:
|
|
223
|
+
validate_automatic_split_size(path, len(self.df))
|
|
224
|
+
|
|
225
|
+
for col in self.molecule_columns:
|
|
226
|
+
self._molecule_smiles[col] = np.array(list(self.df[col]))
|
|
227
|
+
self.molecule_smiles[col] = self._molecule_smiles[col].copy()
|
|
228
|
+
self.valid_smiles[col] = self._molecule_smiles[col].copy()
|
|
229
|
+
self.target_value = torch.tensor(converted_targets.values, dtype=torch.float32)
|
|
230
|
+
if self.target_value.dim() == 1:
|
|
231
|
+
self.target_value = self.target_value.unsqueeze(1)
|
|
232
|
+
self.numeric_inputs = {
|
|
233
|
+
col: np.asarray(converted_numeric[col].values, dtype=np.float32)
|
|
234
|
+
for col in self.numeric_input_columns
|
|
235
|
+
} if self.numeric_input_columns else {}
|
|
236
|
+
self.config.update({"target_dim":self.target_value.shape[1]})
|
|
237
|
+
self.set = self.df.get("set",None)
|
|
238
|
+
self.original_row_indices = np.asarray(self.df.index)
|
|
239
|
+
lengths = {f"molecule:{col}": len(values) for col, values in self._molecule_smiles.items()}
|
|
240
|
+
lengths["target"] = len(self.target_value)
|
|
241
|
+
lengths.update({f"numeric:{col}": len(self.df[col]) for col in self.numeric_input_columns})
|
|
242
|
+
if self.set is not None:
|
|
243
|
+
lengths["set"] = len(self.set)
|
|
244
|
+
validate_aligned_lengths(path, lengths)
|
|
245
|
+
|
|
246
|
+
def write_data_quality_report(self):
|
|
247
|
+
if not self.config.get("data_quality_report", True):
|
|
248
|
+
return None
|
|
249
|
+
if not self.config.get("MODEL_PATH"):
|
|
250
|
+
return None
|
|
251
|
+
report_path, issues_path = write_data_quality_report(
|
|
252
|
+
self.df,
|
|
253
|
+
self.config,
|
|
254
|
+
getattr(self, "graph_errors", None),
|
|
255
|
+
)
|
|
256
|
+
self._output().info(
|
|
257
|
+
f"[Data] Quality report written to {str(report_path)!r}; "
|
|
258
|
+
f"row-level issues: {str(issues_path)!r}."
|
|
259
|
+
)
|
|
260
|
+
return str(report_path), str(issues_path)
|
|
261
|
+
|
|
262
|
+
# Prepare the graph from the smiles, trying to load the graph first, then generate the graph if it does not exist
|
|
263
|
+
def prepare_graph(self):
|
|
264
|
+
for col in self.molecule_columns:
|
|
265
|
+
recipe = build_graph_recipe(self, col)
|
|
266
|
+
path = cache_path(self, col, recipe)
|
|
267
|
+
policy = self.config.get("graph_cache_policy", "v2")
|
|
268
|
+
if policy not in ("v2", "legacy", "regenerate"):
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f"graph_cache_policy must be 'v2', 'legacy', or 'regenerate', got {policy!r}."
|
|
271
|
+
)
|
|
272
|
+
if path.is_file():
|
|
273
|
+
try:
|
|
274
|
+
self.load_graphs(col)
|
|
275
|
+
continue
|
|
276
|
+
except (OSError, RuntimeError, ValueError, TypeError) as exc:
|
|
277
|
+
if policy != "regenerate":
|
|
278
|
+
raise ValueError(
|
|
279
|
+
f"Graph cache {str(path)!r} is corrupt or incompatible: {exc} "
|
|
280
|
+
"Remove the file and rerun, or set graph_cache_policy='regenerate' "
|
|
281
|
+
"to rebuild it from the source CSV."
|
|
282
|
+
) from exc
|
|
283
|
+
warnings.warn(
|
|
284
|
+
f"Regenerating corrupt or incompatible graph cache {str(path)!r}: {exc}",
|
|
285
|
+
RuntimeWarning,
|
|
286
|
+
stacklevel=2,
|
|
287
|
+
)
|
|
288
|
+
legacy_v1, legacy_dgl = legacy_cache_paths(self, col)
|
|
289
|
+
if policy == "legacy" and legacy_v1.is_file():
|
|
290
|
+
warnings.warn(
|
|
291
|
+
f"Loading legacy v1 graph cache {str(legacy_v1)!r}. Its feature recipe and "
|
|
292
|
+
"ISA rule identity cannot be verified; use the default v2 policy when possible.",
|
|
293
|
+
RuntimeWarning,
|
|
294
|
+
stacklevel=2,
|
|
295
|
+
)
|
|
296
|
+
self._load_legacy_graphs(col, legacy_v1)
|
|
297
|
+
continue
|
|
298
|
+
legacy_found = [str(candidate) for candidate in (legacy_v1, legacy_dgl) if candidate.is_file()]
|
|
299
|
+
if legacy_found:
|
|
300
|
+
warnings.warn(
|
|
301
|
+
f"Legacy graph cache files were preserved but will not be loaded: {legacy_found}. "
|
|
302
|
+
f"Generating fingerprinted v2 cache {str(path)!r} from the source CSV.",
|
|
303
|
+
RuntimeWarning,
|
|
304
|
+
stacklevel=2,
|
|
305
|
+
)
|
|
306
|
+
self.generate_graph(col)
|
|
307
|
+
self.save_graphs(col)
|
|
308
|
+
|
|
309
|
+
# Generate the graph from the smiles. the data failed to generate the graph will be saved as an empty graph
|
|
310
|
+
def generate_graph(self,col,graphs=None):
|
|
311
|
+
if graphs is None:
|
|
312
|
+
# Always rebuild this column from scratch to avoid accidental append accumulation.
|
|
313
|
+
self.molecule_graphs[col] = []
|
|
314
|
+
if graphs is not None:
|
|
315
|
+
for smi,g in zip(self._molecule_smiles[col],graphs):
|
|
316
|
+
self.molecule_graphs[col].append(g)
|
|
317
|
+
self.valid_smiles[col].append(smi)
|
|
318
|
+
return
|
|
319
|
+
if len(self._molecule_smiles[col]) < 10:
|
|
320
|
+
iterator = self._molecule_smiles[col]
|
|
321
|
+
else:
|
|
322
|
+
iterator = self._output().progress(
|
|
323
|
+
self._molecule_smiles[col],
|
|
324
|
+
desc=f"Generating {col} graphs",
|
|
325
|
+
)
|
|
326
|
+
for position, smi in enumerate(iterator):
|
|
327
|
+
graphgenerator = self.gg
|
|
328
|
+
if col == 'solvent' and hasattr(self,'gg_solv'):
|
|
329
|
+
graphgenerator = self.__getattribute__('gg_solv')
|
|
330
|
+
else:
|
|
331
|
+
graphgenerator = self.gg
|
|
332
|
+
try:
|
|
333
|
+
g=graphgenerator.get_graph(smi, explicit_h = col in self.explicit_h_columns, **self.config)
|
|
334
|
+
except Exception as e:
|
|
335
|
+
g = graphgenerator.get_empty_graph() # save the empty graph instead of the failed graph
|
|
336
|
+
self._record_graph_error(position, smi, col, str(e))
|
|
337
|
+
self.molecule_graphs[col].append(g)
|
|
338
|
+
|
|
339
|
+
def _record_graph_error(self, position, smiles, molecule_column, reason):
|
|
340
|
+
if not hasattr(self, 'graph_errors'):
|
|
341
|
+
self.graph_errors = []
|
|
342
|
+
if hasattr(self, 'original_row_indices') and len(self.original_row_indices) > position:
|
|
343
|
+
row_index = self.original_row_indices[position]
|
|
344
|
+
if hasattr(row_index, 'item'):
|
|
345
|
+
row_index = row_index.item()
|
|
346
|
+
else:
|
|
347
|
+
row_index = position
|
|
348
|
+
self.graph_errors.append({
|
|
349
|
+
'smiles': smiles,
|
|
350
|
+
'type': molecule_column,
|
|
351
|
+
'reason': reason,
|
|
352
|
+
'row_index': row_index,
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
def write_graph_error_report(self):
|
|
356
|
+
if not self.graph_errors:
|
|
357
|
+
return None
|
|
358
|
+
report_path = os.path.join(self.config['MODEL_PATH'], 'graph_error.csv')
|
|
359
|
+
report = pd.DataFrame(
|
|
360
|
+
self.graph_errors,
|
|
361
|
+
columns=['smiles', 'type', 'reason', 'row_index'],
|
|
362
|
+
)
|
|
363
|
+
report.to_csv(report_path, index=False)
|
|
364
|
+
counts = report.groupby('type', sort=False).size().to_dict()
|
|
365
|
+
unique_rows = report['row_index'].nunique()
|
|
366
|
+
self._output().warning(
|
|
367
|
+
f"[Data] Graph generation excluded {unique_rows} unique CSV rows "
|
|
368
|
+
f"({len(report)} failures by molecule column: {counts}). "
|
|
369
|
+
f"Details: {report_path!r}.",
|
|
370
|
+
RuntimeWarning,
|
|
371
|
+
stacklevel=2,
|
|
372
|
+
)
|
|
373
|
+
return report_path
|
|
374
|
+
|
|
375
|
+
def get_graphs_path(self,col):
|
|
376
|
+
recipe = build_graph_recipe(self, col)
|
|
377
|
+
return str(cache_path(self, col, recipe))
|
|
378
|
+
|
|
379
|
+
# Save the generated graphs
|
|
380
|
+
def save_graphs(self,col):
|
|
381
|
+
recipe = build_graph_recipe(self, col)
|
|
382
|
+
for index, graph in enumerate(self.molecule_graphs[col]):
|
|
383
|
+
validate_graph(graph, recipe, index)
|
|
384
|
+
atomic_save_graph_cache(
|
|
385
|
+
{
|
|
386
|
+
"graph_backend": "pyg",
|
|
387
|
+
"graph_schema_version": 2,
|
|
388
|
+
"recipe": recipe,
|
|
389
|
+
"smiles": list(self._molecule_smiles[col]),
|
|
390
|
+
"graphs": self.molecule_graphs[col],
|
|
391
|
+
"graph_errors": [
|
|
392
|
+
error for error in self.graph_errors if error.get("type") == col
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
cache_path(self, col, recipe),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Load the saved graphs
|
|
399
|
+
def load_graphs(self,col):
|
|
400
|
+
recipe = build_graph_recipe(self, col)
|
|
401
|
+
path = cache_path(self, col, recipe)
|
|
402
|
+
payload = torch.load(path, map_location="cpu", weights_only=False)
|
|
403
|
+
graphs, cached_errors = validate_payload(
|
|
404
|
+
payload,
|
|
405
|
+
recipe,
|
|
406
|
+
self._molecule_smiles[col],
|
|
407
|
+
path,
|
|
408
|
+
)
|
|
409
|
+
self.molecule_graphs[col] = graphs
|
|
410
|
+
self.graph_errors.extend(cached_errors)
|
|
411
|
+
for position, graph in enumerate(self.molecule_graphs[col]):
|
|
412
|
+
if graph.num_nodes == 0 and not any(
|
|
413
|
+
error.get("type") == col and error.get("row_index") == self.original_row_indices[position]
|
|
414
|
+
for error in cached_errors
|
|
415
|
+
):
|
|
416
|
+
self._record_graph_error(
|
|
417
|
+
position,
|
|
418
|
+
self._molecule_smiles[col][position],
|
|
419
|
+
col,
|
|
420
|
+
"Empty graph loaded from cache; the original generation error is unavailable.",
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def _load_legacy_graphs(self, col, path):
|
|
424
|
+
payload = torch.load(path, map_location="cpu", weights_only=False)
|
|
425
|
+
if (
|
|
426
|
+
not isinstance(payload, dict)
|
|
427
|
+
or payload.get("graph_backend") != "pyg"
|
|
428
|
+
or payload.get("graph_schema_version") != 1
|
|
429
|
+
):
|
|
430
|
+
raise ValueError(
|
|
431
|
+
f"Legacy graph cache {str(path)!r} is not a PyG schema-v1 payload. "
|
|
432
|
+
"Use the default v2 policy to regenerate it from the source CSV."
|
|
433
|
+
)
|
|
434
|
+
graphs = payload.get("graphs")
|
|
435
|
+
smiles = payload.get("smiles")
|
|
436
|
+
if not isinstance(graphs, list) or len(graphs) != len(self._molecule_smiles[col]):
|
|
437
|
+
raise ValueError(
|
|
438
|
+
f"Legacy graph cache {str(path)!r} has the wrong graph count. "
|
|
439
|
+
"Regenerate a v2 cache from the source CSV."
|
|
440
|
+
)
|
|
441
|
+
if [str(value) for value in smiles or []] != [
|
|
442
|
+
str(value) for value in self._molecule_smiles[col]
|
|
443
|
+
]:
|
|
444
|
+
raise ValueError(
|
|
445
|
+
f"Legacy graph cache {str(path)!r} ordered SMILES do not match the source CSV."
|
|
446
|
+
)
|
|
447
|
+
recipe = build_graph_recipe(self, col)
|
|
448
|
+
for index, graph in enumerate(graphs):
|
|
449
|
+
validate_graph(graph, recipe, index)
|
|
450
|
+
self.molecule_graphs[col] = graphs
|
|
451
|
+
|
|
452
|
+
# Drop the data with the empty graph
|
|
453
|
+
def drop_none_graph(self):
|
|
454
|
+
_masks = []
|
|
455
|
+
for col in self.molecule_columns:
|
|
456
|
+
_masks.append(np.array([g.num_nodes > 0 for g in self.molecule_graphs[col]]))
|
|
457
|
+
_masks = np.stack(_masks)
|
|
458
|
+
mask = np.all(_masks,axis=0)
|
|
459
|
+
self.masking(mask)
|
|
460
|
+
|
|
461
|
+
# Drop the data with the nan target
|
|
462
|
+
def drop_nan_value(self):
|
|
463
|
+
mask = np.array(~torch.isnan(torch.as_tensor(self.target_value)).all(dim=1))
|
|
464
|
+
self.masking(mask)
|
|
465
|
+
|
|
466
|
+
# Mask the data
|
|
467
|
+
def masking(self,mask):
|
|
468
|
+
mask = np.asarray(mask, dtype=bool)
|
|
469
|
+
|
|
470
|
+
base_len = len(mask)
|
|
471
|
+
for col in self.molecule_columns:
|
|
472
|
+
if len(self._molecule_smiles[col]) != base_len:
|
|
473
|
+
raise ValueError(
|
|
474
|
+
f"Length mismatch before masking for {col}: "
|
|
475
|
+
f"_molecule_smiles={len(self._molecule_smiles[col])}, mask={base_len}."
|
|
476
|
+
)
|
|
477
|
+
if len(self.molecule_graphs[col]) != base_len:
|
|
478
|
+
raise ValueError(
|
|
479
|
+
f"Length mismatch before masking for {col}: "
|
|
480
|
+
f"molecule_graphs={len(self.molecule_graphs[col])}, mask={base_len}."
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
self.target_value = self.target_value[mask]
|
|
484
|
+
if hasattr(self, 'original_row_indices'):
|
|
485
|
+
if len(self.original_row_indices) != base_len:
|
|
486
|
+
raise ValueError(
|
|
487
|
+
f"Length mismatch before masking for original_row_indices: "
|
|
488
|
+
f"rows={len(self.original_row_indices)}, mask={base_len}."
|
|
489
|
+
)
|
|
490
|
+
self.original_row_indices = self.original_row_indices[mask]
|
|
491
|
+
if self.set is not None:
|
|
492
|
+
self.set = self.set[mask]
|
|
493
|
+
|
|
494
|
+
for col in self.molecule_columns:
|
|
495
|
+
self._molecule_smiles[col] = self._molecule_smiles[col][mask]
|
|
496
|
+
self.molecule_smiles[col] = self._molecule_smiles[col].copy()
|
|
497
|
+
if col in self.valid_smiles:
|
|
498
|
+
self.valid_smiles[col] = self.valid_smiles[col][mask]
|
|
499
|
+
self.molecule_graphs[col] = [g for i,g in enumerate(self.molecule_graphs[col]) if mask[i]]
|
|
500
|
+
|
|
501
|
+
if hasattr(self, 'numeric_inputs') and self.numeric_inputs is not None:
|
|
502
|
+
for col in self.numeric_inputs:
|
|
503
|
+
self.numeric_inputs[col] = self.numeric_inputs[col][mask]
|
|
504
|
+
self._mask_additional_features(mask)
|
|
505
|
+
|
|
506
|
+
def _mask_additional_features(self, mask):
|
|
507
|
+
"""Apply the central row mask to model-specific per-row features."""
|
|
508
|
+
return None
|
|
509
|
+
|
|
510
|
+
# drop the invalid data and initialize the dataset
|
|
511
|
+
def prepare_dataset(self,temp=False):
|
|
512
|
+
self.drop_none_graph()
|
|
513
|
+
if not temp:
|
|
514
|
+
self.drop_nan_value()
|
|
515
|
+
self.whole_dataset = self.init_dataset()
|
|
516
|
+
|
|
517
|
+
def init_dataset(self):
|
|
518
|
+
if len(self.numeric_input_columns) > 0:
|
|
519
|
+
if hasattr(self, 'numeric_inputs') and self.numeric_inputs is not None:
|
|
520
|
+
if len(self.numeric_input_columns) != len(self.numeric_inputs):
|
|
521
|
+
raise ValueError(f"Please provide the numeric input for {self.numeric_input_columns}.")
|
|
522
|
+
numeric_inputs = self.numeric_inputs
|
|
523
|
+
else:
|
|
524
|
+
raise ValueError(
|
|
525
|
+
f"Numeric inputs {self.numeric_input_columns!r} were not materialized before dataset creation. "
|
|
526
|
+
"Reload the CSV and verify the configured numeric columns."
|
|
527
|
+
)
|
|
528
|
+
else:
|
|
529
|
+
numeric_inputs = None
|
|
530
|
+
return self.dataset(graphs = self.molecule_graphs,
|
|
531
|
+
smiles = self._molecule_smiles,
|
|
532
|
+
target = self.target_value,
|
|
533
|
+
numeric_inputs = numeric_inputs,
|
|
534
|
+
row_indices = self.original_row_indices,)
|
|
535
|
+
|
|
536
|
+
# Split the dataset into train, val, and test. if the set is given, the dataset will be split according to the set
|
|
537
|
+
def split_data(self):
|
|
538
|
+
import copy
|
|
539
|
+
|
|
540
|
+
def _subset_dataset(indices):
|
|
541
|
+
if hasattr(self.whole_dataset, 'get_subDataset'):
|
|
542
|
+
return self.whole_dataset.get_subDataset(indices)
|
|
543
|
+
dataset = copy.deepcopy(self.whole_dataset)
|
|
544
|
+
if hasattr(dataset, 'subDataset'):
|
|
545
|
+
dataset.subDataset(indices)
|
|
546
|
+
return dataset
|
|
547
|
+
raise AttributeError("Dataset does not support subsetting methods")
|
|
548
|
+
|
|
549
|
+
requested_strategy = self.config.get("split_strategy", "auto")
|
|
550
|
+
strategy = (
|
|
551
|
+
"predefined"
|
|
552
|
+
if requested_strategy == "auto" and self.set is not None
|
|
553
|
+
else "random"
|
|
554
|
+
if requested_strategy == "auto"
|
|
555
|
+
else requested_strategy
|
|
556
|
+
)
|
|
557
|
+
if strategy == "random":
|
|
558
|
+
validate_automatic_split_size(self.config['DATA_PATH'], len(self.whole_dataset))
|
|
559
|
+
|
|
560
|
+
indices = np.arange(len(self.whole_dataset))
|
|
561
|
+
train_idx, test_idx = train_test_split(indices, test_size=0.1, random_state=self.random_seed)
|
|
562
|
+
train_idx, val_idx = train_test_split(train_idx, test_size=1/9, random_state=self.random_seed)
|
|
563
|
+
elif strategy == "predefined":
|
|
564
|
+
if self.set is None:
|
|
565
|
+
raise ValueError(
|
|
566
|
+
"split_strategy='predefined' requires a CSV 'set' column with "
|
|
567
|
+
"train/val/test labels."
|
|
568
|
+
)
|
|
569
|
+
validate_set_labels(self.config['DATA_PATH'], list(self.set))
|
|
570
|
+
train_idx = [i for i,s in enumerate(self.set) if s=="train"]
|
|
571
|
+
val_idx = [i for i,s in enumerate(self.set) if s=="val"]
|
|
572
|
+
test_idx = [i for i,s in enumerate(self.set) if s=="test"]
|
|
573
|
+
elif strategy == "scaffold":
|
|
574
|
+
scaffold_column = self.config.get(
|
|
575
|
+
"scaffold_column", self.molecule_columns[0]
|
|
576
|
+
)
|
|
577
|
+
if scaffold_column not in self.molecule_columns:
|
|
578
|
+
raise ValueError(
|
|
579
|
+
f"scaffold_column {scaffold_column!r} is not one of configured "
|
|
580
|
+
f"molecule_columns {self.molecule_columns!r}."
|
|
581
|
+
)
|
|
582
|
+
(train_idx, val_idx, test_idx), _ = scaffold_split_indices(
|
|
583
|
+
self._molecule_smiles[scaffold_column],
|
|
584
|
+
seed=self.random_seed,
|
|
585
|
+
include_chirality=self.config.get(
|
|
586
|
+
"scaffold_include_chirality", False
|
|
587
|
+
),
|
|
588
|
+
)
|
|
589
|
+
else:
|
|
590
|
+
raise ValueError(
|
|
591
|
+
f"Unknown split_strategy {requested_strategy!r}. Use 'auto', "
|
|
592
|
+
"'random', 'predefined', or 'scaffold'."
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
if (
|
|
596
|
+
self.config.get("MODEL_PATH")
|
|
597
|
+
and hasattr(self, "molecule_columns")
|
|
598
|
+
and hasattr(self, "_molecule_smiles")
|
|
599
|
+
and hasattr(self, "original_row_indices")
|
|
600
|
+
):
|
|
601
|
+
scaffold_column = self.config.get(
|
|
602
|
+
"scaffold_column", self.molecule_columns[0]
|
|
603
|
+
)
|
|
604
|
+
if scaffold_column not in self.molecule_columns:
|
|
605
|
+
raise ValueError(
|
|
606
|
+
f"scaffold_column {scaffold_column!r} is not one of configured "
|
|
607
|
+
f"molecule_columns {self.molecule_columns!r}."
|
|
608
|
+
)
|
|
609
|
+
write_split_report(
|
|
610
|
+
self.config,
|
|
611
|
+
strategy,
|
|
612
|
+
(train_idx, val_idx, test_idx),
|
|
613
|
+
self.original_row_indices,
|
|
614
|
+
self.target_value,
|
|
615
|
+
self.target,
|
|
616
|
+
self._molecule_smiles[scaffold_column],
|
|
617
|
+
scaffold_column,
|
|
618
|
+
include_chirality=self.config.get(
|
|
619
|
+
"scaffold_include_chirality", False
|
|
620
|
+
),
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
self._fit_and_transform_targets(train_idx)
|
|
624
|
+
self.train_dataset = _subset_dataset(train_idx)
|
|
625
|
+
self.val_dataset = _subset_dataset(val_idx)
|
|
626
|
+
self.test_dataset = self.val_dataset if len(test_idx) == 0 else _subset_dataset(test_idx)
|
|
627
|
+
self._output().info(
|
|
628
|
+
"[Data] Dataset split sizes: "
|
|
629
|
+
f"train={len(self.train_dataset)}, "
|
|
630
|
+
f"validation={len(self.val_dataset)}, "
|
|
631
|
+
f"test={len(self.test_dataset)}."
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
def _fit_and_transform_targets(self, train_idx):
|
|
635
|
+
scope = self.config.get('target_scaler_fit_scope', 'train')
|
|
636
|
+
fit_values = self.target_value if scope == 'all' else self.target_value[train_idx]
|
|
637
|
+
if scope == 'all':
|
|
638
|
+
warnings.warn(
|
|
639
|
+
"target_scaler_fit_scope='all' fits target scaling on validation/test rows and may leak "
|
|
640
|
+
"their statistics. Use 'train' for leakage-free training.",
|
|
641
|
+
UserWarning,
|
|
642
|
+
stacklevel=2,
|
|
643
|
+
)
|
|
644
|
+
self.scaler.fit(fit_values)
|
|
645
|
+
transformed = self.scaler.transform(self.target_value)
|
|
646
|
+
self.target_value = torch.as_tensor(transformed, dtype=torch.float32)
|
|
647
|
+
self.whole_dataset.target = self.target_value
|
|
648
|
+
|
|
649
|
+
# Initialize the dataloader
|
|
650
|
+
def init_Dataloader(self,dataset=None,partial_train=1.0, shuffle=False):
|
|
651
|
+
import copy
|
|
652
|
+
|
|
653
|
+
def _subset_for_loader(src_dataset, indices):
|
|
654
|
+
if hasattr(src_dataset, 'get_subDataset'):
|
|
655
|
+
return src_dataset.get_subDataset(indices)
|
|
656
|
+
subset = copy.deepcopy(src_dataset)
|
|
657
|
+
if hasattr(subset, 'subDataset'):
|
|
658
|
+
subset.subDataset(indices)
|
|
659
|
+
return subset
|
|
660
|
+
raise AttributeError("Dataset does not support subsetting methods")
|
|
661
|
+
|
|
662
|
+
if dataset is None:
|
|
663
|
+
dataset = self.whole_dataset
|
|
664
|
+
if partial_train and partial_train<1.0 and partial_train>0:
|
|
665
|
+
if self._partial_train_rng is None:
|
|
666
|
+
subset_idx = np.random.choice(
|
|
667
|
+
len(self.train_dataset),
|
|
668
|
+
int(len(self.train_dataset) * partial_train),
|
|
669
|
+
replace=False,
|
|
670
|
+
)
|
|
671
|
+
else:
|
|
672
|
+
subset_idx = self._partial_train_rng.choice(
|
|
673
|
+
len(self.train_dataset),
|
|
674
|
+
int(len(self.train_dataset) * partial_train),
|
|
675
|
+
replace=False,
|
|
676
|
+
)
|
|
677
|
+
dataset = _subset_for_loader(dataset, subset_idx)
|
|
678
|
+
self._output().info(
|
|
679
|
+
f"[Data] Partial training subset contains {len(dataset)} rows."
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
worker_init_fn = (
|
|
683
|
+
seed_data_loader_worker
|
|
684
|
+
if self.config.get("random_seed") is not None else None
|
|
685
|
+
)
|
|
686
|
+
return DataLoader(dataset,
|
|
687
|
+
batch_size=self.config.get('batch_size',32),
|
|
688
|
+
shuffle=shuffle,
|
|
689
|
+
collate_fn=self.dataset.collate,
|
|
690
|
+
pin_memory=self.config.get('pin_memory',True),
|
|
691
|
+
num_workers=self.config.get('num_workers',0),
|
|
692
|
+
worker_init_fn=worker_init_fn)
|
|
693
|
+
|
|
694
|
+
# (main function) Provide the dataloader for the training, validation, and test
|
|
695
|
+
def get_Dataloaders(self, temp=False):
|
|
696
|
+
self.prepare_dataset(temp=temp)
|
|
697
|
+
if temp:
|
|
698
|
+
loader = self.init_Dataloader(shuffle=False)
|
|
699
|
+
return loader
|
|
700
|
+
else:
|
|
701
|
+
self.split_data()
|
|
702
|
+
self.train_loader = self.init_Dataloader(self.train_dataset,partial_train=self.config.get('partial_train',1.0),shuffle=self.config.get('shuffle',True))
|
|
703
|
+
self.val_loader = self.init_Dataloader(self.val_dataset)
|
|
704
|
+
self.test_loader = self.init_Dataloader(self.test_dataset)
|
|
705
|
+
|
|
706
|
+
return self.train_loader, self.val_loader, self.test_loader
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class MolDataManager_withSolv(MolDataManager):
|
|
710
|
+
"""The class for the management of the molecular data with the solvent, preparing the dataset, and dataloader"""
|
|
711
|
+
def __init__(self, config):
|
|
712
|
+
super().__init__(config)
|
|
713
|
+
self.molecule_columns.append('solvent')
|
|
714
|
+
self.molecule_graphs={col:[] for col in self.molecule_columns}
|
|
715
|
+
|
|
716
|
+
def import_others(self):
|
|
717
|
+
self.graph_type = 'mol'
|
|
718
|
+
self.gg =MolGraphGenerator()
|
|
719
|
+
self.gg_solv =MolGraphGenerator()
|
|
720
|
+
self.dataset =GraphDataset_withSolv
|
|
721
|
+
self.unwrapper = self.dataset.unwrapper
|
|
722
|
+
|
|
723
|
+
def init_temp_data(self,smiles,solvents,**kwargs):
|
|
724
|
+
return super().init_temp_data(smiles, solvents, **kwargs)
|
|
725
|
+
|
|
726
|
+
def init_dataset(self):
|
|
727
|
+
return self.dataset(self.molecule_graphs['compound'], self.molecule_graphs['solvent'], self.target_value, self._molecule_smiles['compound'], self._molecule_smiles['solvent'])
|
|
728
|
+
|
|
729
|
+
def hash(s):
|
|
730
|
+
md5_hash = hashlib.md5()
|
|
731
|
+
md5_hash.update(s.encode('utf-8'))
|
|
732
|
+
hex_digest = md5_hash.hexdigest()
|
|
733
|
+
decimal_hash = int(hex_digest, 16) % (10**12+7)
|
|
734
|
+
|
|
735
|
+
return decimal_hash
|