torch-molecule 0.1.0.dev2__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 (148) hide show
  1. torch_molecule/__init__.py +70 -0
  2. torch_molecule/base/__init__.py +8 -0
  3. torch_molecule/base/base.py +396 -0
  4. torch_molecule/base/encoder.py +44 -0
  5. torch_molecule/base/generator.py +24 -0
  6. torch_molecule/base/old_encoder.py +322 -0
  7. torch_molecule/base/old_predictor.py +904 -0
  8. torch_molecule/base/old_predictor2.py +436 -0
  9. torch_molecule/base/predictor.py +112 -0
  10. torch_molecule/encoder/attrmask/__init__.py +3 -0
  11. torch_molecule/encoder/attrmask/model.py +113 -0
  12. torch_molecule/encoder/attrmask/modeling_attrmask.py +304 -0
  13. torch_molecule/encoder/constant.py +13 -0
  14. torch_molecule/encoder/contextpred/__init__.py +3 -0
  15. torch_molecule/encoder/contextpred/model.py +207 -0
  16. torch_molecule/encoder/contextpred/modeling_contextpred.py +299 -0
  17. torch_molecule/encoder/contextpred/utils.py +206 -0
  18. torch_molecule/encoder/edgepred/__init__.py +3 -0
  19. torch_molecule/encoder/edgepred/model.py +104 -0
  20. torch_molecule/encoder/edgepred/modeling_edgepred.py +285 -0
  21. torch_molecule/encoder/graphmae/__init__.py +3 -0
  22. torch_molecule/encoder/graphmae/dataloader.py +206 -0
  23. torch_molecule/encoder/graphmae/model.py +137 -0
  24. torch_molecule/encoder/graphmae/modeling_graphmae.py +323 -0
  25. torch_molecule/encoder/infograph/__init__.py +3 -0
  26. torch_molecule/encoder/infograph/loss_function.py +128 -0
  27. torch_molecule/encoder/infograph/model.py +92 -0
  28. torch_molecule/encoder/infograph/modeling_infograph.py +301 -0
  29. torch_molecule/encoder/infograph/module.py +31 -0
  30. torch_molecule/encoder/moama/__init__.py +3 -0
  31. torch_molecule/encoder/moama/model.py +131 -0
  32. torch_molecule/encoder/moama/modeling_moama.py +307 -0
  33. torch_molecule/encoder/moama/utils.py +143 -0
  34. torch_molecule/encoder/pretrained/__init__.py +3 -0
  35. torch_molecule/encoder/pretrained/modeling_pretrained.py +274 -0
  36. torch_molecule/encoder/supervised/__init__.py +3 -0
  37. torch_molecule/encoder/supervised/model.py +97 -0
  38. torch_molecule/encoder/supervised/modeling_supervised.py +354 -0
  39. torch_molecule/generator/digress/__init__.py +3 -0
  40. torch_molecule/generator/digress/diffusion.py +194 -0
  41. torch_molecule/generator/digress/modeling_digress.py +503 -0
  42. torch_molecule/generator/digress/transformer.py +360 -0
  43. torch_molecule/generator/digress/utils.py +196 -0
  44. torch_molecule/generator/gdss/__init__.py +3 -0
  45. torch_molecule/generator/gdss/layers.py +257 -0
  46. torch_molecule/generator/gdss/model.py +204 -0
  47. torch_molecule/generator/gdss/modeling_gdss.py +474 -0
  48. torch_molecule/generator/gdss/sde.py +334 -0
  49. torch_molecule/generator/gdss/solver.py +227 -0
  50. torch_molecule/generator/gdss/toremove/ScoreNetworkA.py +167 -0
  51. torch_molecule/generator/gdss/toremove/ScoreNetworkX.py +85 -0
  52. torch_molecule/generator/gdss/utils.py +240 -0
  53. torch_molecule/generator/graph_dit/__init__.py +3 -0
  54. torch_molecule/generator/graph_dit/diffusion.py +188 -0
  55. torch_molecule/generator/graph_dit/modeling_graph_dit.py +581 -0
  56. torch_molecule/generator/graph_dit/transformer.py +207 -0
  57. torch_molecule/generator/graph_dit/utils.py +220 -0
  58. torch_molecule/generator/graph_ga/__init__.py +3 -0
  59. torch_molecule/generator/graph_ga/crossover.py +182 -0
  60. torch_molecule/generator/graph_ga/modeling_graph_ga.py +310 -0
  61. torch_molecule/generator/graph_ga/mutate.py +136 -0
  62. torch_molecule/generator/graph_ga/oracle.py +95 -0
  63. torch_molecule/generator/jtvae/__init__.py +3 -0
  64. torch_molecule/generator/jtvae/jtnn/chemutils.py +450 -0
  65. torch_molecule/generator/jtvae/jtnn/datautils.py +127 -0
  66. torch_molecule/generator/jtvae/jtnn/jtmpn.py +140 -0
  67. torch_molecule/generator/jtvae/jtnn/jtnn_dec.py +350 -0
  68. torch_molecule/generator/jtvae/jtnn/jtnn_enc.py +131 -0
  69. torch_molecule/generator/jtvae/jtnn/mol_tree.py +111 -0
  70. torch_molecule/generator/jtvae/jtnn/mpn.py +129 -0
  71. torch_molecule/generator/jtvae/jtnn/nnutils.py +66 -0
  72. torch_molecule/generator/jtvae/jtnn/vocab.py +31 -0
  73. torch_molecule/generator/jtvae/jtnn_vae.py +246 -0
  74. torch_molecule/generator/jtvae/modeling_jtvae.py +247 -0
  75. torch_molecule/generator/lstm/__init__.py +3 -0
  76. torch_molecule/generator/lstm/action_sampler.py +91 -0
  77. torch_molecule/generator/lstm/lstm.py +59 -0
  78. torch_molecule/generator/lstm/modeling_lstm.py +263 -0
  79. torch_molecule/generator/lstm/smiles_char_dict.py +123 -0
  80. torch_molecule/generator/lstm/utils.py +62 -0
  81. torch_molecule/generator/molgpt/__init__.py +3 -0
  82. torch_molecule/generator/molgpt/dataset.py +87 -0
  83. torch_molecule/generator/molgpt/gpt.py +251 -0
  84. torch_molecule/generator/molgpt/modeling_molgpt.py +427 -0
  85. torch_molecule/generator/pretrained/modeling_pretrained.py +1 -0
  86. torch_molecule/nn/__init__.py +16 -0
  87. torch_molecule/nn/attention.py +63 -0
  88. torch_molecule/nn/embedder.py +183 -0
  89. torch_molecule/nn/gnn.py +631 -0
  90. torch_molecule/nn/mlp.py +34 -0
  91. torch_molecule/predictor/bfgnn/__init__.py +3 -0
  92. torch_molecule/predictor/bfgnn/model.py +116 -0
  93. torch_molecule/predictor/bfgnn/modeling_bfgnn.py +81 -0
  94. torch_molecule/predictor/dir/__init__.py +3 -0
  95. torch_molecule/predictor/dir/model.py +235 -0
  96. torch_molecule/predictor/dir/modeling_dir.py +161 -0
  97. torch_molecule/predictor/dir/utils.py +40 -0
  98. torch_molecule/predictor/gnn/__init__.py +3 -0
  99. torch_molecule/predictor/gnn/model.py +114 -0
  100. torch_molecule/predictor/gnn/modeling_gnn.py +706 -0
  101. torch_molecule/predictor/gnn/old_modeling_gnn.py +707 -0
  102. torch_molecule/predictor/grea/__init__.py +3 -0
  103. torch_molecule/predictor/grea/model.py +190 -0
  104. torch_molecule/predictor/grea/modeling_grea.py +107 -0
  105. torch_molecule/predictor/grea/old1_features.py +229 -0
  106. torch_molecule/predictor/grea/old1_modeling_grea.py +493 -0
  107. torch_molecule/predictor/grea/utils.py +97 -0
  108. torch_molecule/predictor/grin/__init__.py +3 -0
  109. torch_molecule/predictor/grin/model.py +116 -0
  110. torch_molecule/predictor/grin/modeling_grin.py +91 -0
  111. torch_molecule/predictor/irm/__init__.py +3 -0
  112. torch_molecule/predictor/irm/model.py +127 -0
  113. torch_molecule/predictor/irm/modeling_irm.py +179 -0
  114. torch_molecule/predictor/lstm/__init__.py +3 -0
  115. torch_molecule/predictor/lstm/model.py +145 -0
  116. torch_molecule/predictor/lstm/modeling_lstm.py +627 -0
  117. torch_molecule/predictor/lstm/token_from_smiles.py +85 -0
  118. torch_molecule/predictor/old_modeling_graph.py +650 -0
  119. torch_molecule/predictor/rpgnn/__init__.py +3 -0
  120. torch_molecule/predictor/rpgnn/model.py +185 -0
  121. torch_molecule/predictor/rpgnn/modeling_rpgnn.py +76 -0
  122. torch_molecule/predictor/sgir/__init__.py +3 -0
  123. torch_molecule/predictor/sgir/modeling_sgir.py +260 -0
  124. torch_molecule/predictor/sgir/strategy.py +266 -0
  125. torch_molecule/predictor/smiles_transformer/__init__.py +3 -0
  126. torch_molecule/predictor/smiles_transformer/model.py +424 -0
  127. torch_molecule/predictor/smiles_transformer/modeling_transformer.py +252 -0
  128. torch_molecule/predictor/ssr/__init__.py +3 -0
  129. torch_molecule/predictor/ssr/model.py +283 -0
  130. torch_molecule/predictor/ssr/modeling_ssr.py +267 -0
  131. torch_molecule/predictor/ssr/utils.py +185 -0
  132. torch_molecule/utils/__init__.py +31 -0
  133. torch_molecule/utils/checker.py +158 -0
  134. torch_molecule/utils/checkpoint.py +294 -0
  135. torch_molecule/utils/format.py +140 -0
  136. torch_molecule/utils/generic/metrics.py +530 -0
  137. torch_molecule/utils/generic/pseudo_tasks.py +11 -0
  138. torch_molecule/utils/generic/weights.py +30 -0
  139. torch_molecule/utils/graph/features.py +178 -0
  140. torch_molecule/utils/graph/graph_from_smiles.py +134 -0
  141. torch_molecule/utils/graph/graph_to_smiles.py +312 -0
  142. torch_molecule/utils/hf.py +232 -0
  143. torch_molecule/utils/search.py +64 -0
  144. torch_molecule-0.1.0.dev2.dist-info/METADATA +243 -0
  145. torch_molecule-0.1.0.dev2.dist-info/RECORD +148 -0
  146. torch_molecule-0.1.0.dev2.dist-info/WHEEL +5 -0
  147. torch_molecule-0.1.0.dev2.dist-info/licenses/LICENSE +21 -0
  148. torch_molecule-0.1.0.dev2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,650 @@
1
+ import os
2
+ import numpy as np
3
+ from tqdm import tqdm
4
+ from typing import Optional, Union, Dict, Any, Tuple, List
5
+ import warnings
6
+ import datetime
7
+
8
+ import torch
9
+ from torch_geometric.loader import DataLoader
10
+ from torch_geometric.data import Data
11
+
12
+ from .graph_utils.features import smiles_to_graph
13
+ from ..base import BaseMolecularPredictor
14
+
15
+ class MolecularGraphPredictor(BaseMolecularPredictor):
16
+ """This predictor implements a GNN model for molecular property prediction tasks.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ num_tasks: int = 1,
22
+ task_type: str = "classification",
23
+ num_layer: int = 5,
24
+ emb_dim: int = 300,
25
+ gnn_type: str = "gin-virtual",
26
+ drop_ratio: float = 0.5,
27
+ norm_layer: str = "batch_norm",
28
+ batch_size: int = 32,
29
+ epochs: int = 100,
30
+ learning_rate: float = 0.001,
31
+ weight_decay: float = 0.0,
32
+ patience: int = 50,
33
+ device: Optional[str] = None,
34
+ use_lr_scheduler: bool = True,
35
+ scheduler_factor: float = 0.5,
36
+ scheduler_patience: int = 5,
37
+ grad_clip_value: Optional[float] = None,
38
+ verbose: bool = False,
39
+ model_name: str = "graph_predictor",
40
+ criterion = None,
41
+ ):
42
+ super().__init__(num_tasks=num_tasks, task_type=task_type)
43
+
44
+ # Model hyperparameters
45
+ self.num_layer = num_layer
46
+ self.emb_dim = emb_dim
47
+ self.gnn_type = gnn_type
48
+ self.drop_ratio = drop_ratio
49
+ self.norm_layer = norm_layer
50
+
51
+ # Training parameters
52
+ self.batch_size = batch_size
53
+ self.epochs = epochs
54
+ self.learning_rate = learning_rate
55
+ self.weight_decay = weight_decay
56
+ self.patience = patience
57
+ self.use_lr_scheduler = use_lr_scheduler
58
+ self.scheduler_factor = scheduler_factor
59
+ self.scheduler_patience = scheduler_patience
60
+ self.grad_clip_value = grad_clip_value
61
+ self.criterion = criterion if criterion is not None else self._load_default_criterion()
62
+
63
+ # Set device
64
+ self.device = (
65
+ torch.device(device)
66
+ if device
67
+ else torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
68
+ )
69
+
70
+ # Initialize model
71
+ self.model = None
72
+
73
+ self.fitting_loss = []
74
+ self.fitting_loss_mean = float("inf")
75
+ self.fitting_epoch = 0
76
+ self.verbose = verbose
77
+ self.model_name = model_name
78
+
79
+ @staticmethod
80
+ def _get_param_names() -> List[str]:
81
+ """Get parameter names for the estimator.
82
+
83
+ Returns
84
+ -------
85
+ List[str]
86
+ List of parameter names that can be used for model configuration.
87
+ """
88
+ return [
89
+ # Model hyperparameters
90
+ "num_tasks",
91
+ "task_type",
92
+ "num_layer",
93
+ "emb_dim",
94
+ "gnn_type",
95
+ "drop_ratio",
96
+ "norm_layer",
97
+ # Training parameters
98
+ "batch_size",
99
+ "epochs",
100
+ "learning_rate",
101
+ "weight_decay",
102
+ "patience",
103
+ "device",
104
+ "grad_clip_value",
105
+ "criterion",
106
+ # Scheduler parameters
107
+ "use_lr_scheduler",
108
+ "scheduler_factor",
109
+ "scheduler_patience",
110
+ ]
111
+
112
+ def _get_search_parameters(self) -> Dict[str, Union[Tuple[float, float], Tuple[int, int]]]:
113
+ """Get the default hyperparameter search space for auto_fit.
114
+
115
+ Returns
116
+ -------
117
+ Dict[str, Union[Tuple[float, float], Tuple[int, int]]]
118
+ Dictionary containing parameter names and their search ranges:
119
+ - num_layer: Number of GNN layers (2 to 8)
120
+ - emb_dim: Embedding dimension (64 to 512)
121
+ - drop_ratio: Dropout ratio (0.0 to 0.7)
122
+ - learning_rate: Learning rate for optimization (1e-4 to 1e-2)
123
+ - weight_decay: L2 regularization factor (1e-6 to 1e-3)
124
+ """
125
+ return {
126
+ 'num_layer': (2, 8),
127
+ 'emb_dim': (64, 512),
128
+ 'drop_ratio': (0.0, 0.7),
129
+ 'learning_rate': (1e-4, 1e-2),
130
+ 'weight_decay': (1e-6, 1e-3),
131
+ }
132
+
133
+ def _convert_to_pytorch_data(self, X, y=None):
134
+ """Convert numpy arrays to PyTorch data format.
135
+
136
+ Should be implemented by child classes to handle specific molecular
137
+ representations (e.g., SMILES to graphs, conformers to graphs, etc.)
138
+ """
139
+ if self.verbose:
140
+ iterator = tqdm(enumerate(X), desc="Converting molecules to graphs", total=len(X))
141
+ else:
142
+ iterator = enumerate(X)
143
+
144
+ pyg_graph_list = []
145
+ for idx, smiles in iterator:
146
+ if y is not None:
147
+ properties = y[idx]
148
+ else:
149
+ properties = None
150
+ graph = smiles_to_graph(smiles, properties)
151
+ g = Data()
152
+ g.num_nodes = graph["num_nodes"]
153
+ g.edge_index = torch.from_numpy(graph["edge_index"])
154
+
155
+ del graph["num_nodes"]
156
+ del graph["edge_index"]
157
+
158
+ if graph["edge_feat"] is not None:
159
+ g.edge_attr = torch.from_numpy(graph["edge_feat"])
160
+ del graph["edge_feat"]
161
+
162
+ if graph["node_feat"] is not None:
163
+ g.x = torch.from_numpy(graph["node_feat"])
164
+ del graph["node_feat"]
165
+
166
+ if graph["y"] is not None:
167
+ g.y = torch.from_numpy(graph["y"])
168
+ del graph["y"]
169
+
170
+ try:
171
+ g.fp = torch.tensor(graph["fp"], dtype=torch.int8).view(1, -1)
172
+ del graph["fp"]
173
+ except:
174
+ pass
175
+
176
+ pyg_graph_list.append(g)
177
+
178
+ return pyg_graph_list
179
+
180
+ def setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
181
+ """Setup optimization components including optimizer and learning rate scheduler.
182
+
183
+ Returns
184
+ -------
185
+ Tuple[optim.Optimizer, Optional[Any]]
186
+ A tuple containing:
187
+ - The configured optimizer
188
+ - The learning rate scheduler (if enabled, else None)
189
+
190
+ Notes
191
+ -----
192
+ The scheduler returned can be either _LRScheduler or ReduceLROnPlateau,
193
+ hence we use Any as the type hint instead of optim.lr_scheduler._LRScheduler
194
+ """
195
+ optimizer = torch.optim.Adam(
196
+ self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay
197
+ )
198
+ if self.grad_clip_value is not None:
199
+ for group in optimizer.param_groups:
200
+ group.setdefault("max_norm", self.grad_clip_value)
201
+ group.setdefault("norm_type", 2.0)
202
+
203
+ scheduler = None
204
+ if self.use_lr_scheduler:
205
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
206
+ optimizer,
207
+ mode="min",
208
+ factor=self.scheduler_factor,
209
+ patience=self.scheduler_patience,
210
+ min_lr=1e-6,
211
+ cooldown=0,
212
+ eps=1e-8,
213
+ )
214
+
215
+ return optimizer, scheduler
216
+
217
+ def auto_fit(
218
+ self,
219
+ X_train,
220
+ y_train,
221
+ search_parameters: dict = None,
222
+ n_trials: int = 10,
223
+ metric: str = "loss",
224
+ X_val=None,
225
+ y_val=None,
226
+ ) -> "MolecularGraphPredictor":
227
+ """Automatically find the best hyperparameters using Optuna optimization.
228
+
229
+ Parameters
230
+ ----------
231
+ X_train : List[str]
232
+ Training data as SMILES strings
233
+ y_train : Union[List, np.ndarray]
234
+ Training labels
235
+ X_val : List[str], optional
236
+ Validation data
237
+ y_val : Union[List, np.ndarray], optional
238
+ Validation labels
239
+ search_parameters : dict, optional
240
+ Dictionary of parameters to search over
241
+ n_trials : int, default=100
242
+ Number of optimization trials
243
+ metric : str, default="rmse"
244
+ Metric to optimize ("rmse", "mae", "r2", or "loss")
245
+
246
+ Returns
247
+ -------
248
+ self : MolecularGraphPredictor
249
+ Fitted estimator
250
+ """
251
+ import optuna
252
+
253
+ # Default search parameters
254
+ if search_parameters is None:
255
+ search_parameters = self._get_search_parameters()
256
+ else:
257
+ # Validate search parameters against allowed parameters
258
+ valid_params = set(self._get_param_names())
259
+ invalid_params = set(search_parameters.keys()) - valid_params
260
+ if invalid_params:
261
+ raise ValueError(
262
+ f"Invalid search parameters: {invalid_params}. "
263
+ f"Valid parameters are: {valid_params}"
264
+ )
265
+
266
+ # Variables to track best state
267
+ best_score = float('inf')
268
+ best_param_vector = None
269
+ best_trial_params = None
270
+ best_loss = None
271
+ best_epoch = None
272
+
273
+ def objective(trial):
274
+ nonlocal best_score, best_param_vector, best_trial_params, best_loss, best_epoch
275
+
276
+ # Define hyperparameters to optimize
277
+ params = {}
278
+ for param, value_range in search_parameters.items():
279
+ if isinstance(value_range, tuple):
280
+ if param in ['learning_rate', 'weight_decay']:
281
+ params[param] = trial.suggest_float(
282
+ param, value_range[0], value_range[1], log=True
283
+ )
284
+ elif param in ['drop_ratio', 'gamma']:
285
+ params[param] = trial.suggest_float(
286
+ param, value_range[0], value_range[1]
287
+ )
288
+ else:
289
+ params[param] = trial.suggest_int(
290
+ param, value_range[0], value_range[1]
291
+ )
292
+ elif isinstance(value_range, list):
293
+ params[param] = trial.suggest_categorical(param, value_range)
294
+
295
+ # Update model parameters
296
+ self.set_params(**params)
297
+
298
+ try:
299
+ # Train model
300
+ self.fit(X_train, y_train, X_val, y_val)
301
+
302
+ if metric == "loss":
303
+ score = self.fitting_loss_mean
304
+ else:
305
+ y_eval = y_train
306
+ X_eval = X_train
307
+ eval_results = self.predict(X_eval)
308
+ y_pred = eval_results['prediction']
309
+
310
+ # Calculate metric
311
+ if metric == "rmse":
312
+ from sklearn.metrics import mean_squared_error
313
+ score = mean_squared_error(y_eval, y_pred, squared=False)
314
+ elif metric == "mae":
315
+ from sklearn.metrics import mean_absolute_error
316
+ score = mean_absolute_error(y_eval, y_pred)
317
+ elif metric == "r2":
318
+ from sklearn.metrics import r2_score
319
+ score = -r2_score(y_eval, y_pred) # Negative because Optuna minimizes
320
+ else:
321
+ raise ValueError(f"Unknown metric: {metric}")
322
+
323
+ # Update best state if current score is better
324
+ if score < best_score:
325
+ best_score = score
326
+ best_param_vector = torch.nn.utils.parameters_to_vector(self.model.parameters())
327
+ best_trial_params = params
328
+ best_loss = self.fitting_loss
329
+ best_epoch = self.fitting_epoch
330
+
331
+ print(f'done {score}', self)
332
+ return score
333
+
334
+ except Exception as e:
335
+ print(f"Trial failed with error: {str(e)}")
336
+ return float('inf')
337
+
338
+ # Create study
339
+ study = optuna.create_study(direction="minimize")
340
+
341
+ # Run optimization
342
+ study.optimize(
343
+ objective,
344
+ n_trials=n_trials,
345
+ catch=(Exception,),
346
+ show_progress_bar=True if self.verbose else False
347
+ )
348
+
349
+ # Load best parameters and state
350
+ if best_param_vector is not None:
351
+ # Update model hyperparameters
352
+ self.set_params(**best_trial_params)
353
+ # Load best model parameters
354
+ torch.nn.utils.vector_to_parameters(best_param_vector, self.model.parameters())
355
+ # Update training state
356
+ self.fitting_loss = best_loss
357
+ self.fitting_epoch = best_epoch
358
+ self.fitting_loss_mean = float(np.mean(best_loss))
359
+ self.is_fitted_ = True
360
+ else:
361
+ raise RuntimeError("No successful trials completed during optimization")
362
+
363
+ return self
364
+
365
+ def fit(self, X_train: List[str], y_train: Optional[Union[List, np.ndarray]], X_val=None, y_val=None) -> "MolecularGraphPredictor":
366
+ """Fit the model to the data.
367
+
368
+ Parameters
369
+ ----------
370
+ X_train / X_val : list of SMILES strings
371
+ Input molecular structures as SMILES strings
372
+ y_train / y_val : array-like,
373
+ Target values for property prediction
374
+ Returns
375
+ -------
376
+ self : object
377
+ Fitted estimator
378
+ """
379
+ if self.model is None:
380
+ raise RuntimeError("Model not initialized")
381
+
382
+ self.model.initialize_parameters()
383
+ self.model = self.model.to(self.device)
384
+ # Setup training
385
+ optimizer, scheduler = self.setup_optimizers()
386
+ cnt_wait = 0
387
+
388
+ # Convert molecular data to PyTorch Geometric format
389
+ X_train, y_train = self._validate_inputs(X_train, y_train)
390
+ dataset = self._convert_to_pytorch_data(X_train, y_train)
391
+ loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
392
+
393
+ # Initialize best model state
394
+ self.fitting_loss = []
395
+ self.fitting_loss_mean = float("inf")
396
+ self.fitting_epoch = 0
397
+ best_param_vector = None
398
+
399
+ # Training loop
400
+ for epoch in range(self.epochs):
401
+ # Train for one epoch
402
+ losses = self.train_epoch(loader, optimizer)
403
+ loss_mean = np.mean(losses)
404
+ if scheduler:
405
+ scheduler.step(loss_mean)
406
+
407
+ # Update best model
408
+ if loss_mean < self.fitting_loss_mean:
409
+ self.fitting_epoch = epoch
410
+ self.fitting_loss = losses
411
+ self.fitting_loss_mean = loss_mean
412
+ best_param_vector = torch.nn.utils.parameters_to_vector(self.model.parameters())
413
+ cnt_wait = 0
414
+ else:
415
+ cnt_wait += 1
416
+ if cnt_wait > self.patience:
417
+ break
418
+
419
+ if best_param_vector is not None:
420
+ torch.nn.utils.vector_to_parameters(best_param_vector, self.model.parameters())
421
+
422
+ self.is_fitted_ = True
423
+ return self
424
+
425
+ def predict(self, X: List[str]) -> Dict[str, np.ndarray]:
426
+ """Make predictions using the fitted model.
427
+
428
+ Parameters
429
+ ----------
430
+ X : List[str]
431
+ List of SMILES strings to make predictions for
432
+
433
+ Returns
434
+ -------
435
+ Dict[str, np.ndarray]
436
+ Dictionary containing:
437
+ - 'prediction': Model predictions (shape: [n_samples, n_tasks])
438
+
439
+ """
440
+ self._check_is_fitted()
441
+
442
+ # Convert to PyTorch Geometric format and create loader
443
+ X, _ = self._validate_inputs(X)
444
+ dataset = self._convert_to_pytorch_data(X)
445
+ loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
446
+
447
+ if self.model is None:
448
+ raise RuntimeError("Model not initialized")
449
+ # Make predictions
450
+ self.model = self.model.to(self.device)
451
+ self.model.eval()
452
+ predictions = []
453
+ with torch.no_grad():
454
+ for batch in loader:
455
+ batch = batch.to(self.device)
456
+ out = self.model(batch)
457
+ predictions.append(out["prediction"].cpu().numpy())
458
+
459
+ return {
460
+ "prediction": np.concatenate(predictions, axis=0),
461
+ }
462
+
463
+ def train_epoch(self, train_loader, optimizer):
464
+ """Training logic for one epoch.
465
+
466
+ Args:
467
+ train_loader: DataLoader containing training data
468
+ optimizer: Optimizer instance for model parameter updates
469
+
470
+ Returns:
471
+ list: List of loss values for each training step
472
+ """
473
+ self.model.train()
474
+ losses = []
475
+
476
+ iterator = (
477
+ tqdm(train_loader, desc="Training", leave=False)
478
+ if self.verbose
479
+ else train_loader
480
+ )
481
+
482
+ for batch in iterator:
483
+ batch = batch.to(self.device)
484
+ optimizer.zero_grad()
485
+
486
+ # Forward pass and loss computation
487
+ loss = self.model.compute_loss(batch, self.criterion)
488
+
489
+ # Backward pass
490
+ loss.backward()
491
+
492
+ # Compute gradient norm if gradient clipping is enabled
493
+ if self.grad_clip_value is not None:
494
+ torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
495
+
496
+ optimizer.step()
497
+
498
+ losses.append(loss.item())
499
+
500
+ # Update progress bar if using tqdm
501
+ if self.verbose:
502
+ iterator.set_postfix({"loss": f"{loss.item():.4f}"})
503
+
504
+ return losses
505
+
506
+ def save_model(self, path: str) -> None:
507
+ """Save the model to disk.
508
+
509
+ Parameters
510
+ ----------
511
+ path : str
512
+ Path where to save the model
513
+ """
514
+ if not self.is_fitted_:
515
+ raise ValueError("Model must be fitted before saving")
516
+
517
+ if not path.endswith((".pt", ".pth")):
518
+ raise ValueError("Save path should end with '.pt' or '.pth'")
519
+
520
+ # Create directory if it doesn't exist
521
+ os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
522
+
523
+ model_name = os.path.splitext(os.path.basename(path))[0]
524
+ save_dict = {
525
+ "model_state_dict": self.model.state_dict(),
526
+ "hyperparameters": self.get_params(),
527
+ "fitting_epoch": self.fitting_epoch,
528
+ "fitting_loss": self.fitting_loss,
529
+ "fitting_loss_mean": self.fitting_loss_mean,
530
+ "model_name": model_name,
531
+ "date_saved": datetime.datetime.now().isoformat(),
532
+ "version": getattr(self, "__version__", "1.0.0"),
533
+ }
534
+ torch.save(save_dict, path)
535
+
536
+ def load_model(self, path: str, repo: Optional[str] = None) -> "MolecularGraphPredictor":
537
+ """Load a saved model from disk or download from HuggingFace hub.
538
+
539
+ Parameters
540
+ ----------
541
+ path : str
542
+ Path to the saved model file or desired local path for downloaded model
543
+ repo : str, optional
544
+ HuggingFace model repository ID (e.g., 'username/model-name')
545
+ If provided and local path doesn't exist, will attempt to download from hub
546
+
547
+ Returns
548
+ -------
549
+ self : MolecularGraphPredictor
550
+ Updated model instance with loaded weights and parameters
551
+
552
+ Raises
553
+ ------
554
+ FileNotFoundError
555
+ If the model file doesn't exist locally and no repo is provided,
556
+ or if download from repo fails
557
+ ValueError
558
+ If the saved file is corrupted or incompatible
559
+ """
560
+ # First try to load from local path
561
+ if os.path.exists(path):
562
+ try:
563
+ checkpoint = torch.load(path, map_location=self.device)
564
+ except Exception as e:
565
+ raise ValueError(f"Error loading model from {path}: {str(e)}")
566
+
567
+ # If local file doesn't exist and repo is provided, try downloading
568
+ elif repo is not None:
569
+ try:
570
+ from huggingface_hub import hf_hub_download
571
+ # Create directory if it doesn't exist
572
+ os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
573
+
574
+ # Download from HuggingFace using specified parameters
575
+ model_name = os.path.splitext(os.path.basename(path))[0]
576
+ downloaded_path = hf_hub_download(
577
+ repo_id=repo,
578
+ filename=model_name,
579
+ local_dir=os.path.dirname(path),
580
+ local_dir_use_symlinks=False,
581
+ )
582
+
583
+ # Load the downloaded model
584
+ checkpoint = torch.load(downloaded_path, map_location=self.device)
585
+
586
+ except Exception as e:
587
+ if os.path.exists(path):
588
+ os.remove(path) # Clean up partial downloads
589
+ raise FileNotFoundError(
590
+ f"Failed to download model from repository '{repo}': {str(e)}"
591
+ )
592
+
593
+ # If neither local file exists nor repo provided
594
+ else:
595
+ raise FileNotFoundError(f"No model file found at '{path}' and no repository provided")
596
+
597
+ try:
598
+ # Validate checkpoint contents
599
+ required_keys = {
600
+ "model_state_dict",
601
+ "hyperparameters",
602
+ "fitting_loss",
603
+ "fitting_epoch",
604
+ "model_name",
605
+ }
606
+ if not all(key in checkpoint for key in required_keys):
607
+ missing_keys = required_keys - set(checkpoint.keys())
608
+ raise ValueError(f"Checkpoint missing required keys: {missing_keys}")
609
+
610
+ # Validate compatibility of hyperparameters
611
+ for key, value in checkpoint["hyperparameters"].items():
612
+ if hasattr(self, key) and getattr(self, key) != value:
613
+ warnings.warn(
614
+ f"Loaded model has different {key} ({value}) "
615
+ f"than current instance ({getattr(self, key)}). "
616
+ "Updating to match loaded model."
617
+ )
618
+ setattr(self, key, value)
619
+
620
+ # Update model state
621
+ if not hasattr(self, 'model') or self.model is None:
622
+ raise ValueError("Model not initialized. Please initialize model before loading weights.")
623
+
624
+ try:
625
+ self.model.load_state_dict(checkpoint["model_state_dict"])
626
+ except Exception as e:
627
+ raise ValueError(f"Failed to load model state dict: {str(e)}")
628
+
629
+ # Update training state
630
+ self.fitting_epoch = checkpoint.get("fitting_epoch", 0)
631
+ self.fitting_loss = checkpoint.get("fitting_loss", float("inf"))
632
+ self.model_name = checkpoint['model_name']
633
+ self.is_fitted_ = True
634
+
635
+ # Move model to correct device
636
+ self.model = self.model.to(self.device)
637
+
638
+ print(f"Model successfully loaded from {'repository' if repo else 'local path'}")
639
+ return self
640
+
641
+ except Exception as e:
642
+ raise ValueError(f"Error loading model checkpoint: {str(e)}")
643
+
644
+ finally:
645
+ # Clean up any temporary files
646
+ if repo is not None and os.path.exists(downloaded_path) and downloaded_path != path:
647
+ try:
648
+ os.remove(downloaded_path)
649
+ except Exception:
650
+ pass
@@ -0,0 +1,3 @@
1
+ from .modeling_rpgnn import RPGNNMolecularPredictor
2
+
3
+ __all__ = ['RPGNNMolecularPredictor']