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,530 @@
1
+ import warnings
2
+ from typing import Union, Optional
3
+ import numpy as np
4
+ from sklearn.metrics import roc_auc_score as sk_roc_auc_score
5
+ from sklearn.exceptions import UndefinedMetricWarning
6
+ from sklearn.metrics import mean_absolute_error as sk_mae
7
+ from sklearn.metrics import mean_squared_error as sk_mse
8
+ from sklearn.metrics import r2_score as sk_r2_score
9
+
10
+ def sigmoid(x):
11
+ """Numerically stable sigmoid function."""
12
+ return np.where(x >= 0,
13
+ 1 / (1 + np.exp(-x)),
14
+ np.exp(x) / (1 + np.exp(x)))
15
+
16
+ def roc_auc_score(
17
+ y_true: Union[np.ndarray, list],
18
+ y_pred: Union[np.ndarray, list],
19
+ average: bool = True,
20
+ sample_weight: Optional[np.ndarray] = None,
21
+ ) -> Union[float, np.ndarray]:
22
+ """Calculate ROC AUC scores for multi-task binary classification, handling NaN values.
23
+
24
+ For each task dimension, computes AUC score using only the non-NaN samples.
25
+ Tasks with insufficient valid samples or unique labels are masked in the output.
26
+
27
+ Parameters
28
+ ----------
29
+ y_true : Union[np.ndarray, list]
30
+ True binary labels. Shape should be (n_samples, n_tasks)
31
+ y_pred : Union[np.ndarray, list]
32
+ Predicted probabilities. Shape should be (n_samples, n_tasks)
33
+ average : bool, default=True
34
+ If True, return the average ROC AUC score across all valid tasks.
35
+ If False, return individual scores for each task (NaN for invalid tasks).
36
+ sample_weight : Optional[np.ndarray], default=None
37
+ Sample weights for each instance. Shape should be (n_samples,)
38
+
39
+ Returns
40
+ -------
41
+ Union[float, np.ndarray]
42
+ If average=True, returns mean ROC AUC score across all valid tasks.
43
+ If average=False, returns array of ROC AUC scores with NaN for invalid tasks.
44
+
45
+ Raises
46
+ ------
47
+ ValueError
48
+ If input shapes don't match or no valid tasks are found
49
+ TypeError
50
+ If inputs are not of correct type
51
+
52
+ Examples
53
+ --------
54
+ >>> y_true = np.array([[0, 1, np.nan], [1, 0, 1], [1, np.nan, 0], [0, 0, 1]])
55
+ >>> y_pred = np.array([[0.1, 0.8, 0.7], [0.9, 0.2, 0.8], [0.8, 0.7, 0.3], [0.2, 0.1, 0.9]])
56
+ >>> score = roc_auc_score(y_true, y_pred)
57
+ >>> print(f"Average ROC AUC across valid tasks: {score:.3f}")
58
+ """
59
+ # Convert inputs to numpy arrays if needed
60
+ try:
61
+ y_true = np.asarray(y_true)
62
+ y_pred = np.asarray(y_pred)
63
+ if len(y_true.shape) == 1:
64
+ y_true = y_true.reshape(-1, 1)
65
+ except (ValueError, TypeError) as e:
66
+ raise TypeError(f"Could not convert inputs to numpy arrays: {str(e)}")
67
+
68
+ # Validate input shapes
69
+ if y_true.shape != y_pred.shape:
70
+ raise ValueError(
71
+ f"Shape mismatch: y_true shape {y_true.shape} != y_pred shape {y_pred.shape}"
72
+ )
73
+
74
+ if y_true.ndim != 2:
75
+ raise ValueError(
76
+ f"Expected 2D arrays, got y_true.ndim={y_true.ndim}, y_pred.ndim={y_pred.ndim}"
77
+ )
78
+
79
+ if sample_weight is not None:
80
+ sample_weight = np.asarray(sample_weight)
81
+ if sample_weight.shape[0] != y_true.shape[0]:
82
+ raise ValueError(
83
+ f"Sample weight length {sample_weight.shape[0]} != number of samples {y_true.shape[0]}"
84
+ )
85
+
86
+ n_tasks = y_true.shape[1]
87
+ auc_scores = np.full(n_tasks, np.nan) # Initialize with NaN
88
+ valid_task_mask = np.zeros(n_tasks, dtype=bool)
89
+
90
+ # Calculate AUC for each task
91
+ for task_idx in range(n_tasks):
92
+ # Get valid sample mask for this task
93
+ valid_samples = ~np.isnan(y_true[:, task_idx])
94
+
95
+ if not np.any(valid_samples):
96
+ continue # Skip if no valid samples
97
+
98
+ task_true = y_true[valid_samples, task_idx]
99
+ task_pred = y_pred[valid_samples, task_idx]
100
+
101
+ # Get task-specific sample weights if provided
102
+ task_weights = None
103
+ if sample_weight is not None:
104
+ task_weights = sample_weight[valid_samples]
105
+
106
+ # Check for valid binary labels
107
+ unique_labels = np.unique(task_true)
108
+ if len(unique_labels) < 2:
109
+ continue # Skip if not enough unique labels
110
+
111
+ try:
112
+ with warnings.catch_warnings():
113
+ warnings.filterwarnings('ignore', category=UndefinedMetricWarning)
114
+ auc_scores[task_idx] = sk_roc_auc_score(
115
+ task_true,
116
+ task_pred,
117
+ sample_weight=task_weights
118
+ )
119
+ valid_task_mask[task_idx] = True
120
+ except Exception:
121
+ continue # Skip if AUC calculation fails
122
+
123
+ # Check if any valid tasks remain
124
+ if not np.any(valid_task_mask):
125
+ raise ValueError("No valid tasks found for AUC calculation")
126
+
127
+ # Return results based on averaging preference
128
+ if average:
129
+ return float(np.nanmean(auc_scores))
130
+ else:
131
+ return auc_scores
132
+
133
+ def accuracy_score(y_true, logits, avergae=None, thresholds=None, task_weights=None, task_types=None):
134
+ """
135
+ Calculate accuracy for multiple tasks from prediction logits.
136
+
137
+ Parameters:
138
+ -----------
139
+ y_true : numpy.ndarray
140
+ Ground truth labels with shape (n_samples, n_tasks)
141
+ logits : numpy.ndarray
142
+ Prediction logits with shape (n_samples, n_tasks)
143
+ task_types : list or None, optional
144
+ List of task types ('binary' or 'multiclass') for each task
145
+ If None, assumes all tasks are binary
146
+ thresholds : numpy.ndarray or None, optional
147
+ Classification thresholds for binary tasks with shape (n_tasks,)
148
+ If None, uses 0.5 for all binary tasks
149
+ task_weights : numpy.ndarray or None, optional
150
+ Weights for each task with shape (n_tasks,)
151
+ If None, all tasks are weighted equally
152
+
153
+ Returns:
154
+ --------
155
+ dict
156
+ A dictionary containing:
157
+ - 'task_accuracies': Accuracy for each individual task
158
+ - 'weighted_accuracy': Overall weighted accuracy across all tasks
159
+ - 'macro_accuracy': Simple average of all task accuracies
160
+ - 'predictions': Binary predictions after applying activation and thresholds
161
+
162
+ Raises:
163
+ -------
164
+ ValueError
165
+ If input shapes don't match or dimensions are incorrect
166
+ """
167
+ # Convert inputs to numpy arrays if needed
168
+ try:
169
+ y_true = np.asarray(y_true)
170
+ y_pred = np.asarray(y_pred)
171
+ if len(y_true.shape) == 1:
172
+ y_true = y_true.reshape(-1, 1)
173
+ except (ValueError, TypeError) as e:
174
+ raise TypeError(f"Could not convert inputs to numpy arrays: {str(e)}")
175
+
176
+ # Input validation
177
+ if y_true.shape != logits.shape:
178
+ raise ValueError(f"Shape mismatch: y_true {y_true.shape} != logits {logits.shape}")
179
+
180
+ if len(y_true.shape) != 2:
181
+ raise ValueError(f"Expected 2D arrays, got shape {y_true.shape}")
182
+
183
+ n_samples, n_tasks = y_true.shape
184
+
185
+ # Set default task types if none provided
186
+ if task_types is None:
187
+ task_types = ['binary'] * n_tasks
188
+
189
+ if len(task_types) != n_tasks:
190
+ raise ValueError(f"Task types length {len(task_types)} != number of tasks {n_tasks}")
191
+
192
+ # Set default thresholds if none provided
193
+ if thresholds is None:
194
+ thresholds = np.array([0.5] * n_tasks)
195
+ else:
196
+ thresholds = np.array(thresholds)
197
+ if len(thresholds) != n_tasks:
198
+ raise ValueError(f"Thresholds length {len(thresholds)} != number of tasks {n_tasks}")
199
+
200
+ # Set default weights if none provided
201
+ if task_weights is None:
202
+ task_weights = np.ones(n_tasks) / n_tasks
203
+ else:
204
+ if len(task_weights) != n_tasks:
205
+ raise ValueError(f"Task weights length {len(task_weights)} != number of tasks {n_tasks}")
206
+ # Normalize weights to sum to 1
207
+ task_weights = np.array(task_weights) / np.sum(task_weights)
208
+
209
+ # Initialize predictions array
210
+ y_pred = np.zeros_like(logits)
211
+
212
+ # Process each task
213
+ for task_idx in range(n_tasks):
214
+ task_type = task_types[task_idx]
215
+ task_logits = logits[:, task_idx]
216
+
217
+ if task_type == 'binary':
218
+ # Apply sigmoid activation
219
+ probabilities = 1 / (1 + np.exp(-task_logits))
220
+ # Apply threshold
221
+ y_pred[:, task_idx] = (probabilities >= thresholds[task_idx]).astype(int)
222
+ elif task_type == 'multiclass':
223
+ # For multiclass, assume logits are already proper shape and just take argmax
224
+ y_pred[:, task_idx] = np.argmax(task_logits, axis=-1)
225
+ else:
226
+ raise ValueError(f"Unknown task type: {task_type}")
227
+
228
+ # Calculate accuracy for each task
229
+ task_accuracies = np.mean(y_true == y_pred, axis=0)
230
+
231
+ # Calculate weighted average accuracy
232
+ weighted_accuracy = np.sum(task_accuracies * task_weights)
233
+
234
+ # Calculate macro accuracy (simple average)
235
+ if avergae:
236
+ return float(np.mean(task_accuracies))
237
+ else:
238
+ return weighted_accuracy
239
+
240
+
241
+ def mean_absolute_error(
242
+ y_true: Union[np.ndarray, list],
243
+ y_pred: Union[np.ndarray, list],
244
+ average: bool = True,
245
+ sample_weight: Optional[np.ndarray] = None,
246
+ ) -> Union[float, np.ndarray]:
247
+ """Calculate Mean Absolute Error for multi-task regression, handling NaN values.
248
+
249
+ Parameters
250
+ ----------
251
+ y_true : Union[np.ndarray, list]
252
+ Ground truth values. Shape should be (n_samples, n_tasks)
253
+ y_pred : Union[np.ndarray, list]
254
+ Predicted values. Shape should be (n_samples, n_tasks)
255
+ average : bool, default=True
256
+ If True, return the average MAE across all valid tasks.
257
+ If False, return individual MAE for each task (NaN for invalid tasks).
258
+ sample_weight : Optional[np.ndarray], default=None
259
+ Sample weights. Shape should be (n_samples,)
260
+
261
+ Returns
262
+ -------
263
+ Union[float, np.ndarray]
264
+ If average=True, returns mean MAE across all valid tasks.
265
+ If average=False, returns array of MAE scores with NaN for invalid tasks.
266
+ """
267
+ # Convert inputs to numpy arrays if needed
268
+ try:
269
+ y_true = np.asarray(y_true)
270
+ y_pred = np.asarray(y_pred)
271
+ if len(y_true.shape) == 1:
272
+ y_true = y_true.reshape(-1, 1)
273
+ except (ValueError, TypeError) as e:
274
+ raise TypeError(f"Could not convert inputs to numpy arrays: {str(e)}")
275
+
276
+ # Validate input shapes
277
+ if y_true.shape != y_pred.shape:
278
+ raise ValueError(
279
+ f"Shape mismatch: y_true shape {y_true.shape} != y_pred shape {y_pred.shape}"
280
+ )
281
+
282
+ if y_true.ndim != 2:
283
+ raise ValueError(
284
+ f"Expected 2D arrays, got y_true.ndim={y_true.ndim}, y_pred.ndim={y_pred.ndim}"
285
+ )
286
+
287
+ if sample_weight is not None:
288
+ sample_weight = np.asarray(sample_weight)
289
+ if sample_weight.shape[0] != y_true.shape[0]:
290
+ raise ValueError(
291
+ f"Sample weight length {sample_weight.shape[0]} != number of samples {y_true.shape[0]}"
292
+ )
293
+
294
+ n_tasks = y_true.shape[1]
295
+ mae_scores = np.full(n_tasks, np.nan)
296
+ valid_task_mask = np.zeros(n_tasks, dtype=bool)
297
+
298
+ # Calculate MAE for each task
299
+ for task_idx in range(n_tasks):
300
+ # Get valid sample mask for this task
301
+ valid_samples = ~np.isnan(y_true[:, task_idx]) & ~np.isnan(y_pred[:, task_idx])
302
+
303
+ if not np.any(valid_samples):
304
+ continue # Skip if no valid samples
305
+
306
+ task_true = y_true[valid_samples, task_idx]
307
+ task_pred = y_pred[valid_samples, task_idx]
308
+
309
+ # Get task-specific sample weights if provided
310
+ task_weights = None
311
+ if sample_weight is not None:
312
+ task_weights = sample_weight[valid_samples]
313
+
314
+ try:
315
+ mae_scores[task_idx] = sk_mae(
316
+ task_true,
317
+ task_pred,
318
+ sample_weight=task_weights
319
+ )
320
+ valid_task_mask[task_idx] = True
321
+ except Exception:
322
+ continue
323
+
324
+ # Check if any valid tasks remain
325
+ if not np.any(valid_task_mask):
326
+ raise ValueError("No valid tasks found for MAE calculation")
327
+
328
+ # Return results based on averaging preference
329
+ if average:
330
+ return float(np.nanmean(mae_scores))
331
+ else:
332
+ return mae_scores
333
+
334
+ def root_mean_squared_error(y_true, y_pred, average, sample_weight):
335
+ return mean_squared_error(y_true, y_pred, average, sample_weight, squared=False)
336
+
337
+ def mean_squared_error(
338
+ y_true: Union[np.ndarray, list],
339
+ y_pred: Union[np.ndarray, list],
340
+ average: bool = True,
341
+ sample_weight: Optional[np.ndarray] = None,
342
+ squared: bool = True
343
+ ) -> Union[float, np.ndarray]:
344
+ """Calculate Mean Squared Error for multi-task regression, handling NaN values.
345
+
346
+ Parameters
347
+ ----------
348
+ y_true : Union[np.ndarray, list]
349
+ Ground truth values. Shape should be (n_samples, n_tasks)
350
+ y_pred : Union[np.ndarray, list]
351
+ Predicted values. Shape should be (n_samples, n_tasks)
352
+ average : bool, default=True
353
+ If True, return the average MSE across all valid tasks.
354
+ If False, return individual MSE for each task (NaN for invalid tasks).
355
+ sample_weight : Optional[np.ndarray], default=None
356
+ Sample weights. Shape should be (n_samples,)
357
+ squared : bool, default=True
358
+ If True, returns MSE value.
359
+ If False, returns RMSE value.
360
+
361
+ Returns
362
+ -------
363
+ Union[float, np.ndarray]
364
+ If average=True, returns mean MSE/RMSE across all valid tasks.
365
+ If average=False, returns array of MSE/RMSE scores with NaN for invalid tasks.
366
+ """
367
+ # Convert inputs to numpy arrays if needed
368
+ try:
369
+ y_true = np.asarray(y_true)
370
+ y_pred = np.asarray(y_pred)
371
+ if len(y_true.shape) == 1:
372
+ y_true = y_true.reshape(-1, 1)
373
+ except (ValueError, TypeError) as e:
374
+ raise TypeError(f"Could not convert inputs to numpy arrays: {str(e)}")
375
+
376
+ # Validate input shapes
377
+ if y_true.shape != y_pred.shape:
378
+ raise ValueError(
379
+ f"Shape mismatch: y_true shape {y_true.shape} != y_pred shape {y_pred.shape}"
380
+ )
381
+
382
+ if y_true.ndim != 2:
383
+ raise ValueError(
384
+ f"Expected 2D arrays, got y_true.ndim={y_true.ndim}, y_pred.ndim={y_pred.ndim}"
385
+ )
386
+
387
+ if sample_weight is not None:
388
+ sample_weight = np.asarray(sample_weight)
389
+ if sample_weight.shape[0] != y_true.shape[0]:
390
+ raise ValueError(
391
+ f"Sample weight length {sample_weight.shape[0]} != number of samples {y_true.shape[0]}"
392
+ )
393
+
394
+ n_tasks = y_true.shape[1]
395
+ mse_scores = np.full(n_tasks, np.nan)
396
+ valid_task_mask = np.zeros(n_tasks, dtype=bool)
397
+
398
+ # Calculate MSE for each task
399
+ for task_idx in range(n_tasks):
400
+ # Get valid sample mask for this task
401
+ valid_samples = ~np.isnan(y_true[:, task_idx]) & ~np.isnan(y_pred[:, task_idx])
402
+
403
+ if not np.any(valid_samples):
404
+ continue # Skip if no valid samples
405
+
406
+ task_true = y_true[valid_samples, task_idx]
407
+ task_pred = y_pred[valid_samples, task_idx]
408
+
409
+ # Get task-specific sample weights if provided
410
+ task_weights = None
411
+ if sample_weight is not None:
412
+ task_weights = sample_weight[valid_samples]
413
+
414
+ try:
415
+ mse_scores[task_idx] = sk_mse(
416
+ task_true,
417
+ task_pred,
418
+ sample_weight=task_weights,
419
+ )
420
+ valid_task_mask[task_idx] = True
421
+ except Exception:
422
+ continue
423
+
424
+ # Check if any valid tasks remain
425
+ if not np.any(valid_task_mask):
426
+ raise ValueError("No valid tasks found for MSE calculation")
427
+
428
+ # Convert to RMSE if requested
429
+ if not squared:
430
+ mse_scores = np.sqrt(mse_scores)
431
+
432
+ # Return results based on averaging preference
433
+ if average:
434
+ return float(np.nanmean(mse_scores))
435
+ else:
436
+ return mse_scores
437
+
438
+ def r2_score(
439
+ y_true: Union[np.ndarray, list],
440
+ y_pred: Union[np.ndarray, list],
441
+ average: bool = True,
442
+ sample_weight: Optional[np.ndarray] = None,
443
+ ) -> Union[float, np.ndarray]:
444
+ """Calculate R² Score for multi-task regression, handling NaN values.
445
+
446
+ Parameters
447
+ ----------
448
+ y_true : Union[np.ndarray, list]
449
+ Ground truth values. Shape should be (n_samples, n_tasks)
450
+ y_pred : Union[np.ndarray, list]
451
+ Predicted values. Shape should be (n_samples, n_tasks)
452
+ average : bool, default=True
453
+ If True, return the average R² across all valid tasks.
454
+ If False, return individual R² for each task (NaN for invalid tasks).
455
+ sample_weight : Optional[np.ndarray], default=None
456
+ Sample weights. Shape should be (n_samples,)
457
+
458
+ Returns
459
+ -------
460
+ Union[float, np.ndarray]
461
+ If average=True, returns mean R² across all valid tasks.
462
+ If average=False, returns array of R² scores with NaN for invalid tasks.
463
+ """
464
+ # Convert inputs to numpy arrays
465
+ try:
466
+ y_true = np.asarray(y_true)
467
+ y_pred = np.asarray(y_pred)
468
+ if len(y_true.shape) == 1:
469
+ y_true = y_true.reshape(-1, 1)
470
+ except (ValueError, TypeError) as e:
471
+ raise TypeError(f"Could not convert inputs to numpy arrays: {str(e)}")
472
+
473
+ # Validate input shapes
474
+ if y_true.shape != y_pred.shape:
475
+ raise ValueError(
476
+ f"Shape mismatch: y_true shape {y_true.shape} != y_pred shape {y_pred.shape}"
477
+ )
478
+
479
+ if y_true.ndim != 2:
480
+ raise ValueError(
481
+ f"Expected 2D arrays, got y_true.ndim={y_true.ndim}, y_pred.ndim={y_pred.ndim}"
482
+ )
483
+
484
+ if sample_weight is not None:
485
+ sample_weight = np.asarray(sample_weight)
486
+ if sample_weight.shape[0] != y_true.shape[0]:
487
+ raise ValueError(
488
+ f"Sample weight length {sample_weight.shape[0]} != number of samples {y_true.shape[0]}"
489
+ )
490
+
491
+ n_tasks = y_true.shape[1]
492
+ r2_scores = np.full(n_tasks, np.nan)
493
+ valid_task_mask = np.zeros(n_tasks, dtype=bool)
494
+
495
+ # Calculate R² for each task
496
+ for task_idx in range(n_tasks):
497
+ # Get valid sample mask for this task
498
+ valid_samples = ~np.isnan(y_true[:, task_idx]) & ~np.isnan(y_pred[:, task_idx])
499
+
500
+ if not np.any(valid_samples):
501
+ continue # Skip if no valid samples
502
+
503
+ task_true = y_true[valid_samples, task_idx]
504
+ task_pred = y_pred[valid_samples, task_idx]
505
+
506
+ # Get task-specific sample weights if provided
507
+ task_weights = None
508
+ if sample_weight is not None:
509
+ task_weights = sample_weight[valid_samples]
510
+
511
+ try:
512
+ r2_scores[task_idx] = sk_r2_score(
513
+ task_true,
514
+ task_pred,
515
+ sample_weight=task_weights
516
+ )
517
+ valid_task_mask[task_idx] = True
518
+ except Exception:
519
+ continue
520
+
521
+ # Check if any valid tasks remain
522
+ if not np.any(valid_task_mask):
523
+ raise ValueError("No valid tasks found for R² calculation")
524
+
525
+ # Return results based on averaging preference
526
+ if average:
527
+ return float(np.nanmean(r2_scores))
528
+ else:
529
+ return r2_scores
530
+
@@ -0,0 +1,11 @@
1
+ PSEUDOTASK = {
2
+ "morgan": (1024, "classification"),
3
+ "maccs": (167, "classification"),
4
+ "logP": (1, "regression"),
5
+ # "SAS": (1, "regression"),
6
+ # "TPSA": (1, "regression"),
7
+ # "QED": (1, "regression"),
8
+ # "cycle_score": (1, "regression"),
9
+ # "SA_score": (1, "regression"),
10
+ # "weight": (1, "regression"),
11
+ }
@@ -0,0 +1,30 @@
1
+ import torch
2
+
3
+ def init_weights(net, init_type='xavier', init_gain=0.02, verbose=False):
4
+ """Initialize network weights.
5
+ Parameters:
6
+ net (network) -- network to be initialized
7
+ init_type (str) -- the name of an initialization method: normal | xavier | kaiming | orthogonal
8
+ init_gain (float) -- scaling factor for normal, xavier and orthogonal.
9
+ """
10
+ def init_func(m): # define the initialization function
11
+ classname = m.__class__.__name__
12
+ if hasattr(m, 'weight') and (classname.find('Conv') != -1 or classname.find('Linear') != -1):
13
+ if init_type == 'normal':
14
+ torch.nn.init.normal_(m.weight.data, 0.0, init_gain)
15
+ elif init_type == 'xavier':
16
+ torch.nn.init.xavier_normal_(m.weight.data, gain=init_gain)
17
+ elif init_type == 'kaiming':
18
+ torch.nn.init.kaiming_normal_(m.weight.data, a=0, mode='fan_in')
19
+ elif init_type == 'orthogonal':
20
+ torch.nn.init.orthogonal_(m.weight.data)
21
+ else:
22
+ raise NotImplementedError('initialization method [%s] is not implemented' % init_type)
23
+ if hasattr(m, 'bias') and m.bias is not None:
24
+ torch.nn.init.constant_(m.bias.data, 0.0)
25
+ elif classname.find('BatchNorm2d') != -1: # BatchNorm Layer's weight is not a matrix
26
+ torch.nn.init.normal_(m.weight.data, 1.0, init_gain)
27
+ torch.nn.init.constant_(m.bias.data, 0.0)
28
+ if verbose:
29
+ print('initialize network with %s' % init_type)
30
+ net.apply(init_func) # apply the initialization function