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,267 @@
1
+ import os
2
+ import numpy as np
3
+ import warnings
4
+ import datetime
5
+ from tqdm import tqdm
6
+ from typing import Optional, Union, Dict, Any, Tuple, List, Callable, Literal, Type
7
+ from dataclasses import dataclass, field
8
+
9
+ import torch
10
+ from torch_geometric.loader import DataLoader
11
+ from torch_geometric.data import Data
12
+
13
+ from .model import SSR
14
+ from ...utils import graph_from_smiles
15
+ from ..gnn.modeling_gnn import GNNMolecularPredictor
16
+ from ...utils.search import (
17
+ ParameterSpec,
18
+ ParameterType,
19
+ )
20
+
21
+ @dataclass
22
+ class SSRMolecularPredictor(GNNMolecularPredictor):
23
+ """This predictor implements a SizeShiftReg model with the GNN.
24
+
25
+ References
26
+ ----------
27
+ - Paper: SizeShiftReg: a Regularization Method for Improving Size-Generalization in Graph Neural Networks.
28
+ https://arxiv.org/abs/2207.07888
29
+ - Reference Code: https://github.com/DavideBuffelli/SizeShiftReg/tree/main
30
+
31
+ Parameters
32
+ ----------
33
+ coarse_ratios : List[float], default=[0.8, 0.9]
34
+ List of ratios for graph coarsening. Each ratio determines the percentage of
35
+ nodes to keep in the coarsened graph.
36
+ cmd_coeff : float, default=0.1
37
+ Weight for CMD (Central Moment Discrepancy) loss. Controls the strength of
38
+ the size-shift regularization.
39
+ fine_grained : bool, default=True
40
+ Whether to use fine-grained CMD. When True, matches distributions at a more
41
+ detailed level.
42
+ n_moments : int, default=5
43
+ Number of moments to match in the CMD calculation. Higher values capture
44
+ more complex distribution characteristics.
45
+ """
46
+ # SSR-specific parameters
47
+ coarse_ratios: List[float] = field(default_factory=lambda: [0.8, 0.9])
48
+ cmd_coeff: float = field(default=0.1)
49
+ fine_grained: bool = field(default=True)
50
+ n_moments: int = field(default=5)
51
+ coarse_pool: str = field(default='mean')
52
+
53
+ # Other Non-init fields
54
+ model_name: str = "SSRMolecularPredictor"
55
+ model_class: Type[SSR] = field(default=SSR, init=False)
56
+
57
+ def __post_init__(self):
58
+ super().__post_init__()
59
+
60
+ @staticmethod
61
+ def _get_param_names() -> List[str]:
62
+ return GNNMolecularPredictor._get_param_names() + [
63
+ "coarse_ratios",
64
+ "cmd_coeff",
65
+ "fine_grained",
66
+ "n_moments",
67
+ "coarse_pool",
68
+ ]
69
+
70
+ def _get_default_search_space(self):
71
+ search_space = super()._get_default_search_space().copy()
72
+ search_space["cmd_coeff"] = ParameterSpec(ParameterType.FLOAT, (0.01, 1.0))
73
+ search_space["n_moments"] = ParameterSpec(ParameterType.INTEGER, (1, 10))
74
+ return search_space
75
+
76
+ def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
77
+ base_params = super()._get_model_params(checkpoint)
78
+ return base_params
79
+
80
+
81
+ def _convert_to_pytorch_data(self, X, y=None):
82
+ """Convert SMILES to PyTorch Geometric data with coarsened versions, preserving edge attributes."""
83
+ if self.verbose:
84
+ iterator = tqdm(enumerate(X), desc="Converting molecules to graphs", total=len(X))
85
+ else:
86
+ iterator = enumerate(X)
87
+
88
+ pyg_graph_list = []
89
+ for idx, smiles_or_mol in iterator:
90
+ if y is not None:
91
+ properties = y[idx]
92
+ else:
93
+ properties = None
94
+
95
+ # Convert SMILES to graph
96
+ graph = graph_from_smiles(smiles_or_mol, properties, self.augmented_feature)
97
+ g = Data()
98
+ g.num_nodes = graph["num_nodes"]
99
+ g.edge_index = torch.from_numpy(graph["edge_index"])
100
+
101
+ # Standard attributes
102
+ if graph["edge_feat"] is not None:
103
+ g.edge_attr = torch.from_numpy(graph["edge_feat"])
104
+
105
+ if graph["node_feat"] is not None:
106
+ g.x = torch.from_numpy(graph["node_feat"])
107
+
108
+ if graph["y"] is not None:
109
+ g.y = torch.from_numpy(graph["y"])
110
+
111
+ if graph.get("morgan") is not None:
112
+ g.morgan = torch.tensor(graph["morgan"], dtype=torch.int8).view(1, -1)
113
+
114
+ if graph.get("maccs") is not None:
115
+ g.maccs = torch.tensor(graph["maccs"], dtype=torch.int8).view(1, -1)
116
+
117
+ # Add coarsened versions
118
+ for ratio in self.coarse_ratios:
119
+ num_clusters = max(1, int(g.num_nodes * ratio))
120
+ coarse_ratio_postfix = str(int(ratio*100))
121
+
122
+ # Get coarsened graph with edge attributes
123
+ coarse_edge_index, coarse_edge_attr, clusters = self.spectral_graph_coarsening(g, num_clusters)
124
+
125
+ # Add attributes to graph
126
+ setattr(g, f"coarsened_edge_index_{coarse_ratio_postfix}", coarse_edge_index)
127
+ if hasattr(g, 'edge_attr'):
128
+ setattr(g, f"coarsened_edge_attr_{coarse_ratio_postfix}", coarse_edge_attr)
129
+ setattr(g, f"num_coarse_nodes_{coarse_ratio_postfix}", torch.tensor(num_clusters))
130
+ setattr(g, f"clusters_{coarse_ratio_postfix}", clusters)
131
+
132
+ if self.verbose:
133
+ print(f"Processed molecule {idx}: {g.num_nodes} nodes, coarsened versions added")
134
+
135
+ pyg_graph_list.append(g)
136
+
137
+ return pyg_graph_list
138
+
139
+ def spectral_graph_coarsening(self, graph, num_clusters):
140
+ """Coarsen graph based on spectral clustering while preserving edge attributes"""
141
+ # Extract node features and edge information
142
+ edge_index = graph.edge_index
143
+ edge_attr = graph.edge_attr if hasattr(graph, 'edge_attr') else None
144
+ num_nodes = graph.num_nodes
145
+ edge_attr = edge_attr.float()
146
+
147
+ # Convert to adjacency matrix
148
+ adj = torch.zeros((num_nodes, num_nodes), device=edge_index.device)
149
+ adj[edge_index[0], edge_index[1]] = 1
150
+
151
+ # Calculate Laplacian
152
+ degree = adj.sum(dim=1)
153
+ degree_mat = torch.diag(degree)
154
+ laplacian = degree_mat - adj
155
+
156
+ # Compute eigenvectors
157
+ if num_clusters < num_nodes - 1:
158
+ eigvals, eigvecs = torch.linalg.eigh(laplacian)
159
+ # Use smallest non-zero eigenvalues
160
+ indices = torch.argsort(eigvals)[1:num_clusters+1] # Skip first eigenvector
161
+ fiedler_vectors = eigvecs[:, indices]
162
+
163
+ # Use k-means for clustering
164
+ from sklearn.cluster import KMeans
165
+ kmeans = KMeans(n_clusters=num_clusters, random_state=42)
166
+ clusters = kmeans.fit_predict(fiedler_vectors.cpu().numpy())
167
+ clusters = torch.tensor(clusters, device=edge_index.device)
168
+ else:
169
+ clusters = torch.arange(num_nodes, device=edge_index.device)
170
+
171
+ # Create coarsened edge index and attributes
172
+ coarse_edge_index = []
173
+ coarse_edge_attr = []
174
+
175
+ # Store mapping from (cluster_i, cluster_j) to list of edge indices
176
+ cluster_edges = {}
177
+
178
+ # Group edges by their clusters
179
+ for e_idx in range(edge_index.shape[1]):
180
+ src, dst = edge_index[0, e_idx], edge_index[1, e_idx]
181
+ c_src, c_dst = clusters[src], clusters[dst]
182
+
183
+ # Skip self-loops in coarsened graph
184
+ if c_src == c_dst:
185
+ continue
186
+
187
+ # Sort clusters to avoid duplicates
188
+ if c_src > c_dst:
189
+ c_src, c_dst = c_dst, c_src
190
+
191
+ key = (c_src.item(), c_dst.item())
192
+ if key not in cluster_edges:
193
+ cluster_edges[key] = []
194
+
195
+ if edge_attr is not None:
196
+ cluster_edges[key].append(edge_attr[e_idx])
197
+
198
+ # Create coarsened edges
199
+ for (c_src, c_dst), edge_attrs in cluster_edges.items():
200
+ coarse_edge_index.append([c_src, c_dst])
201
+ coarse_edge_index.append([c_dst, c_src]) # Add both directions
202
+
203
+ if edge_attr is not None:
204
+ # Aggregate edge attributes using mean
205
+ mean_attr = torch.stack(edge_attrs).mean(dim=0)
206
+ coarse_edge_attr.append(mean_attr)
207
+ coarse_edge_attr.append(mean_attr) # Same for both directions
208
+
209
+ # Convert to tensors
210
+ if coarse_edge_index:
211
+ coarse_edge_index = torch.tensor(coarse_edge_index, dtype=torch.long,
212
+ device=edge_index.device).t()
213
+ if edge_attr is not None:
214
+ coarse_edge_attr = torch.stack(coarse_edge_attr)
215
+ else:
216
+ coarse_edge_index = torch.zeros((2, 0), dtype=torch.long,
217
+ device=edge_index.device)
218
+ if edge_attr is not None:
219
+ coarse_edge_attr = torch.zeros((0, edge_attr.size(1)),
220
+ device=edge_attr.device)
221
+
222
+ return coarse_edge_index, coarse_edge_attr, clusters
223
+
224
+ def _train_epoch(self, train_loader, optimizer, epoch):
225
+ """Training logic for one epoch with SSR."""
226
+ self.model.train()
227
+ losses = []
228
+ pred_losses = []
229
+ ssr_losses = []
230
+
231
+ iterator = (
232
+ tqdm(train_loader, desc="Training", leave=False)
233
+ if self.verbose
234
+ else train_loader
235
+ )
236
+
237
+ for batch in iterator:
238
+ batch = batch.to(self.device)
239
+ optimizer.zero_grad()
240
+
241
+ # Forward pass and loss computation
242
+ total_loss, pred_loss, ssr_loss = self.model.compute_loss(batch, self.loss_criterion, self.coarse_ratios, self.cmd_coeff, self.fine_grained, self.n_moments)
243
+
244
+ # Backward pass
245
+ total_loss.backward()
246
+
247
+ # Compute gradient norm if gradient clipping is enabled
248
+ if self.grad_clip_value is not None:
249
+ torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
250
+
251
+ optimizer.step()
252
+
253
+ losses.append(total_loss.item())
254
+ pred_losses.append(pred_loss.item())
255
+ ssr_losses.append(ssr_loss.item())
256
+
257
+ # Update progress bar if using tqdm
258
+ if self.verbose:
259
+ iterator.set_postfix({
260
+ "Epoch": epoch,
261
+ "Total loss": f"{total_loss.item():.4f}",
262
+ "Pred loss": f"{pred_loss.item():.4f}",
263
+ "ssr_loss": f"{ssr_loss.item():.4f}"
264
+ })
265
+
266
+ # Return all loss components for logging
267
+ return losses
@@ -0,0 +1,185 @@
1
+ from typing import Optional, Tuple
2
+
3
+ import torch
4
+
5
+ def scatter_sum(src: torch.Tensor,
6
+ index: torch.Tensor,
7
+ dim: int = -1,
8
+ out: Optional[torch.Tensor] = None,
9
+ dim_size: Optional[int] = None) -> torch.Tensor:
10
+ index = broadcast(index, src, dim)
11
+ if out is None:
12
+ size = list(src.size())
13
+ if dim_size is not None:
14
+ size[dim] = dim_size
15
+ elif index.numel() == 0:
16
+ size[dim] = 0
17
+ else:
18
+ size[dim] = int(index.max()) + 1
19
+ out = torch.zeros(size, dtype=src.dtype, device=src.device)
20
+ return out.scatter_add_(dim, index, src)
21
+ else:
22
+ return out.scatter_add_(dim, index, src)
23
+
24
+
25
+ def scatter_add(src: torch.Tensor,
26
+ index: torch.Tensor,
27
+ dim: int = -1,
28
+ out: Optional[torch.Tensor] = None,
29
+ dim_size: Optional[int] = None) -> torch.Tensor:
30
+ return scatter_sum(src, index, dim, out, dim_size)
31
+
32
+
33
+ def scatter_mul(src: torch.Tensor,
34
+ index: torch.Tensor,
35
+ dim: int = -1,
36
+ out: Optional[torch.Tensor] = None,
37
+ dim_size: Optional[int] = None) -> torch.Tensor:
38
+ return torch.ops.torch_scatter.scatter_mul(src, index, dim, out, dim_size)
39
+
40
+
41
+ def scatter_mean(src: torch.Tensor,
42
+ index: torch.Tensor,
43
+ dim: int = -1,
44
+ out: Optional[torch.Tensor] = None,
45
+ dim_size: Optional[int] = None) -> torch.Tensor:
46
+ out = scatter_sum(src, index, dim, out, dim_size)
47
+ dim_size = out.size(dim)
48
+
49
+ index_dim = dim
50
+ if index_dim < 0:
51
+ index_dim = index_dim + src.dim()
52
+ if index.dim() <= index_dim:
53
+ index_dim = index.dim() - 1
54
+
55
+ ones = torch.ones(index.size(), dtype=src.dtype, device=src.device)
56
+ count = scatter_sum(ones, index, index_dim, None, dim_size)
57
+ count[count < 1] = 1
58
+ count = broadcast(count, out, dim)
59
+ if out.is_floating_point():
60
+ out.true_divide_(count)
61
+ else:
62
+ out.div_(count, rounding_mode='floor')
63
+ return out
64
+
65
+
66
+ def scatter_min(
67
+ src: torch.Tensor,
68
+ index: torch.Tensor,
69
+ dim: int = -1,
70
+ out: Optional[torch.Tensor] = None,
71
+ dim_size: Optional[int] = None) -> Tuple[torch.Tensor, torch.Tensor]:
72
+ return torch.ops.torch_scatter.scatter_min(src, index, dim, out, dim_size)
73
+
74
+
75
+ def scatter_max(
76
+ src: torch.Tensor,
77
+ index: torch.Tensor,
78
+ dim: int = -1,
79
+ out: Optional[torch.Tensor] = None,
80
+ dim_size: Optional[int] = None) -> Tuple[torch.Tensor, torch.Tensor]:
81
+ return torch.ops.torch_scatter.scatter_max(src, index, dim, out, dim_size)
82
+
83
+
84
+ def scatter(src: torch.Tensor,
85
+ index: torch.Tensor,
86
+ dim: int = -1,
87
+ out: Optional[torch.Tensor] = None,
88
+ dim_size: Optional[int] = None,
89
+ reduce: str = "sum") -> torch.Tensor:
90
+ r"""
91
+ |
92
+
93
+ .. image:: https://raw.githubusercontent.com/rusty1s/pytorch_scatter/
94
+ master/docs/source/_figures/add.svg?sanitize=true
95
+ :align: center
96
+ :width: 400px
97
+
98
+ |
99
+
100
+ Reduces all values from the :attr:`src` tensor into :attr:`out` at the
101
+ indices specified in the :attr:`index` tensor along a given axis
102
+ :attr:`dim`.
103
+ For each value in :attr:`src`, its output index is specified by its index
104
+ in :attr:`src` for dimensions outside of :attr:`dim` and by the
105
+ corresponding value in :attr:`index` for dimension :attr:`dim`.
106
+ The applied reduction is defined via the :attr:`reduce` argument.
107
+
108
+ Formally, if :attr:`src` and :attr:`index` are :math:`n`-dimensional
109
+ tensors with size :math:`(x_0, ..., x_{i-1}, x_i, x_{i+1}, ..., x_{n-1})`
110
+ and :attr:`dim` = `i`, then :attr:`out` must be an :math:`n`-dimensional
111
+ tensor with size :math:`(x_0, ..., x_{i-1}, y, x_{i+1}, ..., x_{n-1})`.
112
+ Moreover, the values of :attr:`index` must be between :math:`0` and
113
+ :math:`y - 1`, although no specific ordering of indices is required.
114
+ The :attr:`index` tensor supports broadcasting in case its dimensions do
115
+ not match with :attr:`src`.
116
+
117
+ For one-dimensional tensors with :obj:`reduce="sum"`, the operation
118
+ computes
119
+
120
+ .. math::
121
+ \mathrm{out}_i = \mathrm{out}_i + \sum_j~\mathrm{src}_j
122
+
123
+ where :math:`\sum_j` is over :math:`j` such that
124
+ :math:`\mathrm{index}_j = i`.
125
+
126
+ .. note::
127
+
128
+ This operation is implemented via atomic operations on the GPU and is
129
+ therefore **non-deterministic** since the order of parallel operations
130
+ to the same value is undetermined.
131
+ For floating-point variables, this results in a source of variance in
132
+ the result.
133
+
134
+ :param src: The source tensor.
135
+ :param index: The indices of elements to scatter.
136
+ :param dim: The axis along which to index. (default: :obj:`-1`)
137
+ :param out: The destination tensor.
138
+ :param dim_size: If :attr:`out` is not given, automatically create output
139
+ with size :attr:`dim_size` at dimension :attr:`dim`.
140
+ If :attr:`dim_size` is not given, a minimal sized output tensor
141
+ according to :obj:`index.max() + 1` is returned.
142
+ :param reduce: The reduce operation (:obj:`"sum"`, :obj:`"mul"`,
143
+ :obj:`"mean"`, :obj:`"min"` or :obj:`"max"`). (default: :obj:`"sum"`)
144
+
145
+ :rtype: :class:`Tensor`
146
+
147
+ .. code-block:: python
148
+
149
+ from torch_scatter import scatter
150
+
151
+ src = torch.randn(10, 6, 64)
152
+ index = torch.tensor([0, 1, 0, 1, 2, 1])
153
+
154
+ # Broadcasting in the first and last dim.
155
+ out = scatter(src, index, dim=1, reduce="sum")
156
+
157
+ print(out.size())
158
+
159
+ .. code-block::
160
+
161
+ torch.Size([10, 3, 64])
162
+ """
163
+ if reduce == 'sum' or reduce == 'add':
164
+ return scatter_sum(src, index, dim, out, dim_size)
165
+ if reduce == 'mul':
166
+ return scatter_mul(src, index, dim, out, dim_size)
167
+ elif reduce == 'mean':
168
+ return scatter_mean(src, index, dim, out, dim_size)
169
+ elif reduce == 'min':
170
+ return scatter_min(src, index, dim, out, dim_size)[0]
171
+ elif reduce == 'max':
172
+ return scatter_max(src, index, dim, out, dim_size)[0]
173
+ else:
174
+ raise ValueError
175
+
176
+ def broadcast(src: torch.Tensor, other: torch.Tensor, dim: int):
177
+ if dim < 0:
178
+ dim = other.dim() + dim
179
+ if src.dim() == 1:
180
+ for _ in range(0, dim):
181
+ src = src.unsqueeze(0)
182
+ for _ in range(src.dim(), other.dim()):
183
+ src = src.unsqueeze(-1)
184
+ src = src.expand(other.size())
185
+ return src
@@ -0,0 +1,31 @@
1
+ from .generic.weights import init_weights
2
+ from .generic.metrics import (
3
+ roc_auc_score,
4
+ accuracy_score,
5
+ mean_squared_error,
6
+ mean_absolute_error,
7
+ root_mean_squared_error,
8
+ r2_score,
9
+ )
10
+ from .generic.pseudo_tasks import PSEUDOTASK
11
+ from .graph.graph_from_smiles import graph_from_smiles
12
+ from .graph.graph_to_smiles import graph_to_smiles
13
+ from .graph.features import get_atom_feature_dims, get_bond_feature_dims
14
+
15
+ __all__ = [
16
+ "init_weights",
17
+ # metric
18
+ "roc_auc_score",
19
+ "accuracy_score",
20
+ "mean_squared_error",
21
+ "mean_absolute_error",
22
+ "root_mean_squared_error",
23
+ "r2_score",
24
+ # graph
25
+ "graph_from_smiles",
26
+ "graph_to_smiles",
27
+ "get_atom_feature_dims",
28
+ "get_bond_feature_dims",
29
+ # pseudo_tasks
30
+ "PSEUDOTASK",
31
+ ]
@@ -0,0 +1,158 @@
1
+ import warnings
2
+ import numpy as np
3
+ from rdkit import Chem
4
+ from typing import Optional, Union, List, Tuple
5
+
6
+ class MolecularInputChecker:
7
+ """
8
+ Class for validating input data used in molecular models.
9
+ """
10
+
11
+ @staticmethod
12
+ def validate_smiles(
13
+ smiles: str,
14
+ idx: int
15
+ ) -> Tuple[bool, Optional[str], Optional[Chem.Mol]]:
16
+ """Validate a single SMILES string at a given index.
17
+
18
+ Parameters
19
+ ----------
20
+ smiles : str
21
+ The SMILES string to validate
22
+ idx : int
23
+ The index of the SMILES string in the original list
24
+
25
+ Returns
26
+ -------
27
+ Tuple[bool, Optional[str], Optional[Chem.Mol]]
28
+ A tuple containing:
29
+
30
+ - A boolean indicating whether the SMILES string is valid
31
+ - A string describing the error if the SMILES is invalid, or None if valid
32
+ - The RDKit Mol object if valid, or None if invalid
33
+ """
34
+ if not smiles or not smiles.strip():
35
+ return False, f"Empty SMILES at index {idx}", None
36
+
37
+ try:
38
+ mol = Chem.MolFromSmiles(smiles)
39
+ if mol is None:
40
+ return False, f"Invalid SMILES structure at index {idx}: {smiles}", None
41
+ return True, None, mol
42
+ except Exception as e:
43
+ return False, f"RDKit error at index {idx}: {str(e)}", None
44
+
45
+ @staticmethod
46
+ def validate_inputs(
47
+ X: List[str],
48
+ y: Optional[Union[List, np.ndarray]] = None,
49
+ num_task: int = 0,
50
+ num_pretask: int = 0,
51
+ return_rdkit_mol: bool = True
52
+ ) -> Tuple[Union[List[str], List["Chem.Mol"]], Optional[np.ndarray]]:
53
+ """Validate a list of SMILES strings, and optionally validate a target array.
54
+
55
+ Parameters
56
+ ----------
57
+ X : List[str]
58
+ List of SMILES strings
59
+ y : Optional[Union[List, np.ndarray]], optional
60
+ Optional target values, by default None
61
+ num_task : int, optional
62
+ Total number of tasks; used to check dimensions of y, by default 0
63
+ num_pretask : int, optional
64
+ Number of (pseudo)-tasks that are predefined in the modeling;
65
+ used to check dimensions of y. Preliminarily used in supervised pretraining,
66
+ by default 0
67
+ return_rdkit_mol : bool, optional
68
+ If True, convert SMILES to RDKit Mol objects, by default True
69
+
70
+ Returns
71
+ -------
72
+ Tuple[Union[List[str], List["Chem.Mol"]], Optional[np.ndarray]]
73
+ A tuple containing:
74
+
75
+ - The original or converted SMILES (RDKit Mol objects if return_rdkit_mol=True)
76
+ - The target array as a numpy array, or None if y was not provided
77
+
78
+ Raises
79
+ ------
80
+ ValueError
81
+ If SMILES or target dimensions are invalid
82
+ """
83
+ if not isinstance(X, list):
84
+ raise ValueError("X must be a list of SMILES strings.")
85
+
86
+ if not all(isinstance(s, str) for s in X):
87
+ raise ValueError("All elements in X must be strings.")
88
+
89
+ invalid_smiles = []
90
+ rdkit_mols = []
91
+ for i, smiles in enumerate(X):
92
+ is_valid, error_msg, mol = MolecularInputChecker.validate_smiles(smiles, i)
93
+ if not is_valid:
94
+ invalid_smiles.append(error_msg)
95
+ else:
96
+ rdkit_mols.append(mol)
97
+
98
+ if invalid_smiles:
99
+ raise ValueError("Invalid SMILES found:\n" + "\n".join(invalid_smiles))
100
+
101
+ if y is not None:
102
+ try:
103
+ y = np.asarray(y, dtype=np.float32)
104
+ except Exception as e:
105
+ raise ValueError(f"Could not convert y to numpy array: {str(e)}")
106
+
107
+ if len(y.shape) == 1:
108
+ if num_task - num_pretask != 1:
109
+ raise ValueError(
110
+ f"1D target array provided but num_task is {num_task - num_pretask}. "
111
+ "For multiple tasks, y must be 2D."
112
+ )
113
+ y = y.reshape(-1, 1)
114
+
115
+ if len(y.shape) != 2:
116
+ raise ValueError(
117
+ "y must be 1D (single task) or 2D (multiple tasks). "
118
+ f"Got shape {y.shape}."
119
+ )
120
+
121
+ if y.shape[0] != len(X):
122
+ raise ValueError(
123
+ f"Number of samples in y ({y.shape[0]}) must match length of X ({len(X)})."
124
+ )
125
+
126
+ if y.shape[1] != num_task - num_pretask:
127
+ raise ValueError(
128
+ f"Second dimension of y ({y.shape[1]}) must match num_task ({num_task - num_pretask})."
129
+ )
130
+
131
+ inf_mask = np.isinf(y)
132
+ if np.any(inf_mask):
133
+ inf_indices = np.where(inf_mask)
134
+ warnings.warn(
135
+ f"Infinite values found in y at indices: {list(zip(*inf_indices))}. "
136
+ "Converting to NaN.",
137
+ RuntimeWarning,
138
+ )
139
+ y = y.astype(float)
140
+ y[inf_mask] = np.nan
141
+
142
+ # nan_mask = np.isnan(y)
143
+ # if np.any(nan_mask):
144
+ # nan_counts = np.sum(nan_mask, axis=0)
145
+ # nan_percentages = (nan_counts / len(X)) * 100
146
+ # task_warnings = []
147
+ # for task_idx, (count, percentage) in enumerate(zip(nan_counts, nan_percentages)):
148
+ # if count > 0:
149
+ # task_warnings.append(f"Task {task_idx}: {count} NaNs ({percentage:.1f}%)")
150
+
151
+ # warnings.warn(
152
+ # "NaN values present in y:\n"
153
+ # + "\n".join(task_warnings)
154
+ # + "\nSamples with NaN will be ignored or cause issues unless handled.",
155
+ # RuntimeWarning,
156
+ # )
157
+
158
+ return rdkit_mols if return_rdkit_mol else X, y