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.
- torch_molecule/__init__.py +70 -0
- torch_molecule/base/__init__.py +8 -0
- torch_molecule/base/base.py +396 -0
- torch_molecule/base/encoder.py +44 -0
- torch_molecule/base/generator.py +24 -0
- torch_molecule/base/old_encoder.py +322 -0
- torch_molecule/base/old_predictor.py +904 -0
- torch_molecule/base/old_predictor2.py +436 -0
- torch_molecule/base/predictor.py +112 -0
- torch_molecule/encoder/attrmask/__init__.py +3 -0
- torch_molecule/encoder/attrmask/model.py +113 -0
- torch_molecule/encoder/attrmask/modeling_attrmask.py +304 -0
- torch_molecule/encoder/constant.py +13 -0
- torch_molecule/encoder/contextpred/__init__.py +3 -0
- torch_molecule/encoder/contextpred/model.py +207 -0
- torch_molecule/encoder/contextpred/modeling_contextpred.py +299 -0
- torch_molecule/encoder/contextpred/utils.py +206 -0
- torch_molecule/encoder/edgepred/__init__.py +3 -0
- torch_molecule/encoder/edgepred/model.py +104 -0
- torch_molecule/encoder/edgepred/modeling_edgepred.py +285 -0
- torch_molecule/encoder/graphmae/__init__.py +3 -0
- torch_molecule/encoder/graphmae/dataloader.py +206 -0
- torch_molecule/encoder/graphmae/model.py +137 -0
- torch_molecule/encoder/graphmae/modeling_graphmae.py +323 -0
- torch_molecule/encoder/infograph/__init__.py +3 -0
- torch_molecule/encoder/infograph/loss_function.py +128 -0
- torch_molecule/encoder/infograph/model.py +92 -0
- torch_molecule/encoder/infograph/modeling_infograph.py +301 -0
- torch_molecule/encoder/infograph/module.py +31 -0
- torch_molecule/encoder/moama/__init__.py +3 -0
- torch_molecule/encoder/moama/model.py +131 -0
- torch_molecule/encoder/moama/modeling_moama.py +307 -0
- torch_molecule/encoder/moama/utils.py +143 -0
- torch_molecule/encoder/pretrained/__init__.py +3 -0
- torch_molecule/encoder/pretrained/modeling_pretrained.py +274 -0
- torch_molecule/encoder/supervised/__init__.py +3 -0
- torch_molecule/encoder/supervised/model.py +97 -0
- torch_molecule/encoder/supervised/modeling_supervised.py +354 -0
- torch_molecule/generator/digress/__init__.py +3 -0
- torch_molecule/generator/digress/diffusion.py +194 -0
- torch_molecule/generator/digress/modeling_digress.py +503 -0
- torch_molecule/generator/digress/transformer.py +360 -0
- torch_molecule/generator/digress/utils.py +196 -0
- torch_molecule/generator/gdss/__init__.py +3 -0
- torch_molecule/generator/gdss/layers.py +257 -0
- torch_molecule/generator/gdss/model.py +204 -0
- torch_molecule/generator/gdss/modeling_gdss.py +474 -0
- torch_molecule/generator/gdss/sde.py +334 -0
- torch_molecule/generator/gdss/solver.py +227 -0
- torch_molecule/generator/gdss/toremove/ScoreNetworkA.py +167 -0
- torch_molecule/generator/gdss/toremove/ScoreNetworkX.py +85 -0
- torch_molecule/generator/gdss/utils.py +240 -0
- torch_molecule/generator/graph_dit/__init__.py +3 -0
- torch_molecule/generator/graph_dit/diffusion.py +188 -0
- torch_molecule/generator/graph_dit/modeling_graph_dit.py +581 -0
- torch_molecule/generator/graph_dit/transformer.py +207 -0
- torch_molecule/generator/graph_dit/utils.py +220 -0
- torch_molecule/generator/graph_ga/__init__.py +3 -0
- torch_molecule/generator/graph_ga/crossover.py +182 -0
- torch_molecule/generator/graph_ga/modeling_graph_ga.py +310 -0
- torch_molecule/generator/graph_ga/mutate.py +136 -0
- torch_molecule/generator/graph_ga/oracle.py +95 -0
- torch_molecule/generator/jtvae/__init__.py +3 -0
- torch_molecule/generator/jtvae/jtnn/chemutils.py +450 -0
- torch_molecule/generator/jtvae/jtnn/datautils.py +127 -0
- torch_molecule/generator/jtvae/jtnn/jtmpn.py +140 -0
- torch_molecule/generator/jtvae/jtnn/jtnn_dec.py +350 -0
- torch_molecule/generator/jtvae/jtnn/jtnn_enc.py +131 -0
- torch_molecule/generator/jtvae/jtnn/mol_tree.py +111 -0
- torch_molecule/generator/jtvae/jtnn/mpn.py +129 -0
- torch_molecule/generator/jtvae/jtnn/nnutils.py +66 -0
- torch_molecule/generator/jtvae/jtnn/vocab.py +31 -0
- torch_molecule/generator/jtvae/jtnn_vae.py +246 -0
- torch_molecule/generator/jtvae/modeling_jtvae.py +247 -0
- torch_molecule/generator/lstm/__init__.py +3 -0
- torch_molecule/generator/lstm/action_sampler.py +91 -0
- torch_molecule/generator/lstm/lstm.py +59 -0
- torch_molecule/generator/lstm/modeling_lstm.py +263 -0
- torch_molecule/generator/lstm/smiles_char_dict.py +123 -0
- torch_molecule/generator/lstm/utils.py +62 -0
- torch_molecule/generator/molgpt/__init__.py +3 -0
- torch_molecule/generator/molgpt/dataset.py +87 -0
- torch_molecule/generator/molgpt/gpt.py +251 -0
- torch_molecule/generator/molgpt/modeling_molgpt.py +427 -0
- torch_molecule/generator/pretrained/modeling_pretrained.py +1 -0
- torch_molecule/nn/__init__.py +16 -0
- torch_molecule/nn/attention.py +63 -0
- torch_molecule/nn/embedder.py +183 -0
- torch_molecule/nn/gnn.py +631 -0
- torch_molecule/nn/mlp.py +34 -0
- torch_molecule/predictor/bfgnn/__init__.py +3 -0
- torch_molecule/predictor/bfgnn/model.py +116 -0
- torch_molecule/predictor/bfgnn/modeling_bfgnn.py +81 -0
- torch_molecule/predictor/dir/__init__.py +3 -0
- torch_molecule/predictor/dir/model.py +235 -0
- torch_molecule/predictor/dir/modeling_dir.py +161 -0
- torch_molecule/predictor/dir/utils.py +40 -0
- torch_molecule/predictor/gnn/__init__.py +3 -0
- torch_molecule/predictor/gnn/model.py +114 -0
- torch_molecule/predictor/gnn/modeling_gnn.py +706 -0
- torch_molecule/predictor/gnn/old_modeling_gnn.py +707 -0
- torch_molecule/predictor/grea/__init__.py +3 -0
- torch_molecule/predictor/grea/model.py +190 -0
- torch_molecule/predictor/grea/modeling_grea.py +107 -0
- torch_molecule/predictor/grea/old1_features.py +229 -0
- torch_molecule/predictor/grea/old1_modeling_grea.py +493 -0
- torch_molecule/predictor/grea/utils.py +97 -0
- torch_molecule/predictor/grin/__init__.py +3 -0
- torch_molecule/predictor/grin/model.py +116 -0
- torch_molecule/predictor/grin/modeling_grin.py +91 -0
- torch_molecule/predictor/irm/__init__.py +3 -0
- torch_molecule/predictor/irm/model.py +127 -0
- torch_molecule/predictor/irm/modeling_irm.py +179 -0
- torch_molecule/predictor/lstm/__init__.py +3 -0
- torch_molecule/predictor/lstm/model.py +145 -0
- torch_molecule/predictor/lstm/modeling_lstm.py +627 -0
- torch_molecule/predictor/lstm/token_from_smiles.py +85 -0
- torch_molecule/predictor/old_modeling_graph.py +650 -0
- torch_molecule/predictor/rpgnn/__init__.py +3 -0
- torch_molecule/predictor/rpgnn/model.py +185 -0
- torch_molecule/predictor/rpgnn/modeling_rpgnn.py +76 -0
- torch_molecule/predictor/sgir/__init__.py +3 -0
- torch_molecule/predictor/sgir/modeling_sgir.py +260 -0
- torch_molecule/predictor/sgir/strategy.py +266 -0
- torch_molecule/predictor/smiles_transformer/__init__.py +3 -0
- torch_molecule/predictor/smiles_transformer/model.py +424 -0
- torch_molecule/predictor/smiles_transformer/modeling_transformer.py +252 -0
- torch_molecule/predictor/ssr/__init__.py +3 -0
- torch_molecule/predictor/ssr/model.py +283 -0
- torch_molecule/predictor/ssr/modeling_ssr.py +267 -0
- torch_molecule/predictor/ssr/utils.py +185 -0
- torch_molecule/utils/__init__.py +31 -0
- torch_molecule/utils/checker.py +158 -0
- torch_molecule/utils/checkpoint.py +294 -0
- torch_molecule/utils/format.py +140 -0
- torch_molecule/utils/generic/metrics.py +530 -0
- torch_molecule/utils/generic/pseudo_tasks.py +11 -0
- torch_molecule/utils/generic/weights.py +30 -0
- torch_molecule/utils/graph/features.py +178 -0
- torch_molecule/utils/graph/graph_from_smiles.py +134 -0
- torch_molecule/utils/graph/graph_to_smiles.py +312 -0
- torch_molecule/utils/hf.py +232 -0
- torch_molecule/utils/search.py +64 -0
- torch_molecule-0.1.0.dev2.dist-info/METADATA +243 -0
- torch_molecule-0.1.0.dev2.dist-info/RECORD +148 -0
- torch_molecule-0.1.0.dev2.dist-info/WHEEL +5 -0
- torch_molecule-0.1.0.dev2.dist-info/licenses/LICENSE +21 -0
- 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
|