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,299 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
from tqdm import tqdm
|
|
4
|
+
from typing import Optional, Union, Dict, Any, Tuple, List, Callable, Literal, Type
|
|
5
|
+
import warnings
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
from torch_geometric.loader import DataLoader
|
|
10
|
+
from torch_geometric.data import Data
|
|
11
|
+
|
|
12
|
+
from .model import GNN
|
|
13
|
+
from ..constant import GNN_ENCODER_MODELS, GNN_ENCODER_READOUTS, GNN_ENCODER_PARAMS
|
|
14
|
+
from ...base import BaseMolecularEncoder
|
|
15
|
+
from ...utils import graph_from_smiles
|
|
16
|
+
|
|
17
|
+
ALLOWABLE_ENCODER_MODELS = GNN_ENCODER_MODELS
|
|
18
|
+
ALLOWABLE_ENCODER_READOUTS = GNN_ENCODER_READOUTS
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ContextPredMolecularEncoder(BaseMolecularEncoder):
|
|
22
|
+
"""This encoder implements a GNN-based model for molecular representation learning
|
|
23
|
+
using the context prediction pretraining strategy.
|
|
24
|
+
|
|
25
|
+
References
|
|
26
|
+
----------
|
|
27
|
+
- Paper: Strategies for Pre-training Graph Neural Networks (ICLR 2020) https://arxiv.org/abs/1905.12265
|
|
28
|
+
- Code: https://github.com/snap-stanford/pretrain-gnns/tree/master/chem
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
mode : str, default="cbow"
|
|
33
|
+
Type of context prediction task. One of ["cbow", "skipgram"].
|
|
34
|
+
context_size : int, default=2
|
|
35
|
+
Size of the context window used for predicting node-level features.
|
|
36
|
+
neg_samples : int, default=1
|
|
37
|
+
Number of negative samples used in the training objective.
|
|
38
|
+
num_layer : int, default=3
|
|
39
|
+
Number of GNN layers.
|
|
40
|
+
hidden_size : int, default=300
|
|
41
|
+
Dimension of hidden node features.
|
|
42
|
+
drop_ratio : float, default=0.5
|
|
43
|
+
Dropout probability.
|
|
44
|
+
norm_layer : str, default="batch_norm"
|
|
45
|
+
Type of normalization layer to use. One of ["batch_norm", "layer_norm", "instance_norm", "graph_norm", "size_norm", "pair_norm"].
|
|
46
|
+
encoder_type : str, default="gin-virtual"
|
|
47
|
+
Type of GNN architecture to use. One of ["gin-virtual", "gcn-virtual", "gin", "gcn"].
|
|
48
|
+
readout : str, default="sum"
|
|
49
|
+
Method for aggregating node features to obtain graph-level representations. One of ["sum", "mean", "max"].
|
|
50
|
+
batch_size : int, default=128
|
|
51
|
+
Number of samples per batch for training.
|
|
52
|
+
epochs : int, default=500
|
|
53
|
+
Maximum number of training epochs.
|
|
54
|
+
learning_rate : float, default=0.001
|
|
55
|
+
Learning rate for the optimizer.
|
|
56
|
+
grad_clip_value : float, optional
|
|
57
|
+
Maximum norm of gradients for gradient clipping.
|
|
58
|
+
weight_decay : float, default=0.0
|
|
59
|
+
L2 regularization strength.
|
|
60
|
+
use_lr_scheduler : bool, default=False
|
|
61
|
+
Whether to use a learning rate scheduler during training.
|
|
62
|
+
scheduler_factor : float, default=0.5
|
|
63
|
+
Factor by which to reduce learning rate when plateau is reached.
|
|
64
|
+
scheduler_patience : int, default=5
|
|
65
|
+
Number of epochs with no improvement after which learning rate will be reduced.
|
|
66
|
+
verbose : bool, default=False
|
|
67
|
+
Whether to print progress information during training.
|
|
68
|
+
"""
|
|
69
|
+
# Task related parameters
|
|
70
|
+
mode: str = "cbow" # cbow or skipgram
|
|
71
|
+
context_size: int = 2
|
|
72
|
+
neg_samples: int = 1
|
|
73
|
+
|
|
74
|
+
# Model parameters
|
|
75
|
+
num_layer: int = 3
|
|
76
|
+
hidden_size: int = 300
|
|
77
|
+
drop_ratio: float = 0.5
|
|
78
|
+
norm_layer: str = "batch_norm"
|
|
79
|
+
|
|
80
|
+
encoder_type: str = "gin-virtual"
|
|
81
|
+
readout: str = "sum"
|
|
82
|
+
|
|
83
|
+
# Training parameters
|
|
84
|
+
batch_size: int = 128
|
|
85
|
+
epochs: int = 500
|
|
86
|
+
learning_rate: float = 0.001
|
|
87
|
+
grad_clip_value: Optional[float] = None
|
|
88
|
+
weight_decay: float = 0.0
|
|
89
|
+
|
|
90
|
+
# Scheduler parameters
|
|
91
|
+
use_lr_scheduler: bool = False
|
|
92
|
+
scheduler_factor: float = 0.5
|
|
93
|
+
scheduler_patience: int = 5
|
|
94
|
+
|
|
95
|
+
# Other parameters
|
|
96
|
+
verbose: bool = False
|
|
97
|
+
model_name: str = "ContextPredMolecularEncoder"
|
|
98
|
+
|
|
99
|
+
# Non-init fields
|
|
100
|
+
fitting_loss: List[float] = field(default_factory=list, init=False)
|
|
101
|
+
fitting_epoch: int = field(default=0, init=False)
|
|
102
|
+
model_class: Type[GNN] = field(default=GNN, init=False)
|
|
103
|
+
|
|
104
|
+
def __post_init__(self):
|
|
105
|
+
"""Initialize the model after dataclass initialization."""
|
|
106
|
+
super().__post_init__()
|
|
107
|
+
if self.encoder_type not in ALLOWABLE_ENCODER_MODELS:
|
|
108
|
+
raise ValueError(f"Invalid encoder_model: {self.encoder_type}. Currently only {ALLOWABLE_ENCODER_MODELS} are supported.")
|
|
109
|
+
if self.readout not in ALLOWABLE_ENCODER_READOUTS:
|
|
110
|
+
raise ValueError(f"Invalid encoder_readout: {self.readout}. Currently only {ALLOWABLE_ENCODER_READOUTS} are supported.")
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _get_param_names() -> List[str]:
|
|
114
|
+
return ["mode", "context_size", "neg_samples"] + GNN_ENCODER_PARAMS.copy()
|
|
115
|
+
|
|
116
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
117
|
+
params = {
|
|
118
|
+
"num_layer": self.num_layer,
|
|
119
|
+
"hidden_size": self.hidden_size,
|
|
120
|
+
"drop_ratio": self.drop_ratio,
|
|
121
|
+
"norm_layer": self.norm_layer,
|
|
122
|
+
"readout": self.readout,
|
|
123
|
+
"encoder_type": self.encoder_type,
|
|
124
|
+
"mode": self.mode,
|
|
125
|
+
"context_size": self.context_size,
|
|
126
|
+
"neg_samples": self.neg_samples
|
|
127
|
+
}
|
|
128
|
+
if checkpoint is not None:
|
|
129
|
+
if "hyperparameters" not in checkpoint:
|
|
130
|
+
raise ValueError("Checkpoint missing 'hyperparameters' key")
|
|
131
|
+
hyperparameters = checkpoint["hyperparameters"]
|
|
132
|
+
params = {k: hyperparameters.get(k, v) for k, v in params.items()}
|
|
133
|
+
|
|
134
|
+
return params
|
|
135
|
+
|
|
136
|
+
def _convert_to_pytorch_data(self, X):
|
|
137
|
+
"""Convert numpy arrays to PyTorch Geometric data format.
|
|
138
|
+
"""
|
|
139
|
+
if self.verbose:
|
|
140
|
+
iterator = tqdm(enumerate(X), desc="Converting molecules to graphs", total=len(X))
|
|
141
|
+
else:
|
|
142
|
+
iterator = enumerate(X)
|
|
143
|
+
|
|
144
|
+
pyg_graph_list = []
|
|
145
|
+
for idx, smiles_or_mol in iterator:
|
|
146
|
+
graph = graph_from_smiles(smiles_or_mol, None)
|
|
147
|
+
g = Data()
|
|
148
|
+
g.num_nodes = graph["num_nodes"]
|
|
149
|
+
g.edge_index = torch.from_numpy(graph["edge_index"])
|
|
150
|
+
|
|
151
|
+
del graph["num_nodes"]
|
|
152
|
+
del graph["edge_index"]
|
|
153
|
+
|
|
154
|
+
if graph["edge_feat"] is not None:
|
|
155
|
+
g.edge_attr = torch.from_numpy(graph["edge_feat"])
|
|
156
|
+
del graph["edge_feat"]
|
|
157
|
+
|
|
158
|
+
if graph["node_feat"] is not None:
|
|
159
|
+
g.x = torch.from_numpy(graph["node_feat"])
|
|
160
|
+
del graph["node_feat"]
|
|
161
|
+
|
|
162
|
+
pyg_graph_list.append(g)
|
|
163
|
+
|
|
164
|
+
return pyg_graph_list
|
|
165
|
+
|
|
166
|
+
def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
167
|
+
"""Setup optimization components including optimizer and learning rate scheduler.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
optimizer = torch.optim.Adam(self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay)
|
|
171
|
+
scheduler = None
|
|
172
|
+
if self.use_lr_scheduler:
|
|
173
|
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
174
|
+
optimizer,
|
|
175
|
+
mode="min",
|
|
176
|
+
factor=self.scheduler_factor,
|
|
177
|
+
patience=self.scheduler_patience,
|
|
178
|
+
min_lr=1e-6,
|
|
179
|
+
cooldown=0,
|
|
180
|
+
eps=1e-8,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return optimizer, scheduler
|
|
184
|
+
|
|
185
|
+
def fit(
|
|
186
|
+
self,
|
|
187
|
+
X_train: List[str]
|
|
188
|
+
) -> "ContextPredMolecularEncoder":
|
|
189
|
+
"""Fit the model to the training data with optional validation set.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
X_train : List[str]
|
|
194
|
+
Training set input molecular structures as SMILES strings
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
self : ContextPredMolecularEncoder
|
|
198
|
+
Fitted estimator
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
self._initialize_model(self.model_class)
|
|
202
|
+
self.model.initialize_parameters()
|
|
203
|
+
optimizer, scheduler = self._setup_optimizers()
|
|
204
|
+
|
|
205
|
+
# Prepare datasets and loaders
|
|
206
|
+
X_train, _ = self._validate_inputs(X_train, return_rdkit_mol=True)
|
|
207
|
+
train_dataset = self._convert_to_pytorch_data(X_train)
|
|
208
|
+
train_loader = DataLoader(
|
|
209
|
+
train_dataset,
|
|
210
|
+
batch_size=self.batch_size,
|
|
211
|
+
shuffle=True,
|
|
212
|
+
num_workers=0
|
|
213
|
+
)
|
|
214
|
+
self.fitting_loss = []
|
|
215
|
+
|
|
216
|
+
for epoch in range(self.epochs):
|
|
217
|
+
# Training phase
|
|
218
|
+
train_losses = self._train_epoch(train_loader, optimizer, epoch)
|
|
219
|
+
self.fitting_loss.append(np.mean(train_losses))
|
|
220
|
+
if scheduler:
|
|
221
|
+
scheduler.step(np.mean(train_losses))
|
|
222
|
+
|
|
223
|
+
self.fitting_epoch = epoch
|
|
224
|
+
self.is_fitted_ = True
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def _train_epoch(self, train_loader, optimizer, epoch):
|
|
228
|
+
"""Training logic for one epoch.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
train_loader: DataLoader containing training data
|
|
232
|
+
optimizer: Optimizer instance for model parameter updates
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
list: List of loss values for each training step
|
|
236
|
+
"""
|
|
237
|
+
self.model.train()
|
|
238
|
+
losses = []
|
|
239
|
+
|
|
240
|
+
iterator = (
|
|
241
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
242
|
+
if self.verbose
|
|
243
|
+
else train_loader
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
for batch in iterator:
|
|
247
|
+
batch = batch.to(self.device)
|
|
248
|
+
optimizer.zero_grad()
|
|
249
|
+
loss = self.model.compute_loss(batch)
|
|
250
|
+
loss.backward()
|
|
251
|
+
if self.grad_clip_value is not None:
|
|
252
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
|
|
253
|
+
optimizer.step()
|
|
254
|
+
losses.append(loss.item())
|
|
255
|
+
|
|
256
|
+
# Update progress bar if using tqdm
|
|
257
|
+
if self.verbose:
|
|
258
|
+
iterator.set_postfix({"Epoch": f"{epoch}", "Loss": f"{loss.item():.4f}"})
|
|
259
|
+
|
|
260
|
+
return losses
|
|
261
|
+
|
|
262
|
+
def encode(self, X: List[str], return_type: Literal["np", "pt"] = "pt") -> Union[np.ndarray, torch.Tensor]:
|
|
263
|
+
"""Encode molecules into vector representations.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
X : List[str]
|
|
268
|
+
List of SMILES strings
|
|
269
|
+
return_type : Literal["np", "pt"], default="pt"
|
|
270
|
+
Return type of the representations
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
representations : ndarray or torch.Tensor
|
|
275
|
+
Molecular representations
|
|
276
|
+
"""
|
|
277
|
+
self._check_is_fitted()
|
|
278
|
+
|
|
279
|
+
# Convert to PyTorch Geometric format and create loader
|
|
280
|
+
X, _ = self._validate_inputs(X, return_rdkit_mol=True)
|
|
281
|
+
dataset = self._convert_to_pytorch_data(X)
|
|
282
|
+
loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
|
|
283
|
+
|
|
284
|
+
if self.model is None:
|
|
285
|
+
raise RuntimeError("Model not initialized")
|
|
286
|
+
|
|
287
|
+
# Generate encodings
|
|
288
|
+
self.model = self.model.to(self.device)
|
|
289
|
+
self.model.eval()
|
|
290
|
+
encodings = []
|
|
291
|
+
with torch.no_grad():
|
|
292
|
+
for batch in tqdm(loader, disable=not self.verbose):
|
|
293
|
+
batch = batch.to(self.device)
|
|
294
|
+
out = self.model(batch)
|
|
295
|
+
encodings.append(out["graph"].cpu())
|
|
296
|
+
|
|
297
|
+
# Concatenate and convert to requested format
|
|
298
|
+
encodings = torch.cat(encodings, dim=0)
|
|
299
|
+
return encodings if return_type == "pt" else encodings.numpy()
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import copy
|
|
3
|
+
import random
|
|
4
|
+
import networkx as nx
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from torch_geometric.data import Data
|
|
8
|
+
|
|
9
|
+
def cumsum(self, key, item):
|
|
10
|
+
r"""If :obj:`True`, the attribute :obj:`key` with content :obj:`item`
|
|
11
|
+
should be added up cumulatively before concatenated together.
|
|
12
|
+
.. note::
|
|
13
|
+
This method is for internal use only, and should only be overridden
|
|
14
|
+
if the batch concatenation process is corrupted for a specific data
|
|
15
|
+
attribute.
|
|
16
|
+
"""
|
|
17
|
+
return key in ["edge_index", "edge_index_substruct", "edge_index_context", "overlap_context_substruct_idx", "center_substruct_idx"]
|
|
18
|
+
|
|
19
|
+
class ExtractSubstructureContextPair:
|
|
20
|
+
def __init__(self, k, l1, l2):
|
|
21
|
+
"""
|
|
22
|
+
Randomly selects a node from the data object, and adds attributes
|
|
23
|
+
that contain the substructure that corresponds to k hop neighbours
|
|
24
|
+
rooted at the node, and the context substructures that corresponds to
|
|
25
|
+
the subgraph that is between l1 and l2 hops away from the
|
|
26
|
+
root node.
|
|
27
|
+
:param k:
|
|
28
|
+
:param l1:
|
|
29
|
+
:param l2:
|
|
30
|
+
"""
|
|
31
|
+
self.k = k
|
|
32
|
+
self.l1 = l1
|
|
33
|
+
self.l2 = l2
|
|
34
|
+
|
|
35
|
+
# for the special case of 0, addresses the quirk with
|
|
36
|
+
# single_source_shortest_path_length
|
|
37
|
+
if self.k == 0:
|
|
38
|
+
self.k = -1
|
|
39
|
+
if self.l1 == 0:
|
|
40
|
+
self.l1 = -1
|
|
41
|
+
if self.l2 == 0:
|
|
42
|
+
self.l2 = -1
|
|
43
|
+
|
|
44
|
+
def __call__(self, data, root_idx=None):
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
:param data: pytorch geometric data object
|
|
48
|
+
:param root_idx: If None, then randomly samples an atom idx.
|
|
49
|
+
Otherwise sets atom idx of root (for debugging only)
|
|
50
|
+
:return: None. Creates new attributes in original data object:
|
|
51
|
+
data.center_substruct_idx
|
|
52
|
+
data.x_substruct
|
|
53
|
+
data.edge_attr_substruct
|
|
54
|
+
data.edge_index_substruct
|
|
55
|
+
data.x_context
|
|
56
|
+
data.edge_attr_context
|
|
57
|
+
data.edge_index_context
|
|
58
|
+
data.overlap_context_substruct_idx
|
|
59
|
+
"""
|
|
60
|
+
num_atoms = data.x.size()[0]
|
|
61
|
+
if root_idx == None:
|
|
62
|
+
root_idx = random.sample(range(num_atoms), 1)[0]
|
|
63
|
+
|
|
64
|
+
G = graph_data_obj_to_nx_simple(data) # same ordering as input data obj
|
|
65
|
+
|
|
66
|
+
# Get k-hop subgraph rooted at specified atom idx
|
|
67
|
+
substruct_node_idxes = nx.single_source_shortest_path_length(G,
|
|
68
|
+
root_idx,
|
|
69
|
+
self.k).keys()
|
|
70
|
+
if len(substruct_node_idxes) > 0:
|
|
71
|
+
substruct_G = G.subgraph(substruct_node_idxes)
|
|
72
|
+
substruct_G, substruct_node_map = reset_idxes(substruct_G) # need
|
|
73
|
+
# to reset node idx to 0 -> num_nodes - 1, otherwise data obj does not
|
|
74
|
+
# make sense, since the node indices in data obj must start at 0
|
|
75
|
+
substruct_data = nx_to_graph_data_obj_simple(substruct_G)
|
|
76
|
+
data.substruct_node_idxes = torch.tensor(list(substruct_node_idxes))
|
|
77
|
+
data.x_substruct = substruct_data.x
|
|
78
|
+
data.edge_attr_substruct = substruct_data.edge_attr
|
|
79
|
+
data.edge_index_substruct = substruct_data.edge_index
|
|
80
|
+
data.center_substruct_idx = torch.tensor([substruct_node_map[
|
|
81
|
+
root_idx]]) # need
|
|
82
|
+
# to convert center idx from original graph node ordering to the
|
|
83
|
+
# new substruct node ordering
|
|
84
|
+
|
|
85
|
+
# Get subgraphs that is between l1 and l2 hops away from the root node
|
|
86
|
+
l1_node_idxes = nx.single_source_shortest_path_length(G, root_idx,
|
|
87
|
+
self.l1).keys()
|
|
88
|
+
l2_node_idxes = nx.single_source_shortest_path_length(G, root_idx,
|
|
89
|
+
self.l2).keys()
|
|
90
|
+
context_node_idxes = set(l1_node_idxes).symmetric_difference(
|
|
91
|
+
set(l2_node_idxes))
|
|
92
|
+
if len(context_node_idxes) > 0:
|
|
93
|
+
context_G = G.subgraph(context_node_idxes)
|
|
94
|
+
context_G, context_node_map = reset_idxes(context_G) # need to
|
|
95
|
+
# reset node idx to 0 -> num_nodes - 1, otherwise data obj does not
|
|
96
|
+
# make sense, since the node indices in data obj must start at 0
|
|
97
|
+
context_data = nx_to_graph_data_obj_simple(context_G)
|
|
98
|
+
data.context_node_idxes = torch.tensor(list(context_node_idxes))
|
|
99
|
+
data.x_context = context_data.x
|
|
100
|
+
data.edge_attr_context = context_data.edge_attr
|
|
101
|
+
data.edge_index_context = context_data.edge_index
|
|
102
|
+
|
|
103
|
+
# Get indices of overlapping nodes between substruct and context,
|
|
104
|
+
# WRT context ordering
|
|
105
|
+
context_substruct_overlap_idxes = list(set(
|
|
106
|
+
context_node_idxes).intersection(set(substruct_node_idxes)))
|
|
107
|
+
if len(context_substruct_overlap_idxes) > 0:
|
|
108
|
+
context_substruct_overlap_idxes_reorder = [context_node_map[old_idx]
|
|
109
|
+
for
|
|
110
|
+
old_idx in
|
|
111
|
+
context_substruct_overlap_idxes]
|
|
112
|
+
# need to convert the overlap node idxes, which is from the
|
|
113
|
+
# original graph node ordering to the new context node ordering
|
|
114
|
+
data.overlap_context_substruct_idx = \
|
|
115
|
+
torch.tensor(context_substruct_overlap_idxes_reorder)
|
|
116
|
+
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
def reset_idxes(G):
|
|
120
|
+
"""
|
|
121
|
+
Resets node indices such that they are numbered from 0 to num_nodes - 1
|
|
122
|
+
:param G:
|
|
123
|
+
:return: copy of G with relabelled node indices, mapping
|
|
124
|
+
"""
|
|
125
|
+
mapping = {}
|
|
126
|
+
for new_idx, old_idx in enumerate(G.nodes()):
|
|
127
|
+
mapping[old_idx] = new_idx
|
|
128
|
+
new_G = nx.relabel_nodes(G, mapping, copy=True)
|
|
129
|
+
return new_G, mapping
|
|
130
|
+
|
|
131
|
+
def graph_data_obj_to_nx_simple(data):
|
|
132
|
+
"""
|
|
133
|
+
Converts graph Data object required by the pytorch geometric package to
|
|
134
|
+
network x data object. NB: Uses simplified atom and bond features,
|
|
135
|
+
and represent as indices. NB: possible issues with recapitulating relative
|
|
136
|
+
stereochemistry since the edges in the nx object are unordered.
|
|
137
|
+
:param data: pytorch geometric Data object
|
|
138
|
+
:return: network x object
|
|
139
|
+
"""
|
|
140
|
+
G = nx.Graph()
|
|
141
|
+
|
|
142
|
+
# atoms
|
|
143
|
+
atom_features = data.x.cpu().numpy()
|
|
144
|
+
num_atoms = atom_features.shape[0]
|
|
145
|
+
for i in range(num_atoms):
|
|
146
|
+
atomic_num_idx, chirality_tag_idx, _, _, _, _, _, _, _ = atom_features[i]
|
|
147
|
+
G.add_node(i, atom_num_idx=atomic_num_idx, chirality_tag_idx=chirality_tag_idx)
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
# bonds
|
|
151
|
+
edge_index = data.edge_index.cpu().numpy()
|
|
152
|
+
edge_attr = data.edge_attr.cpu().numpy()
|
|
153
|
+
num_bonds = edge_index.shape[1]
|
|
154
|
+
for j in range(0, num_bonds, 2):
|
|
155
|
+
begin_idx = int(edge_index[0, j])
|
|
156
|
+
end_idx = int(edge_index[1, j])
|
|
157
|
+
bond_type_idx, bond_dir_idx, _ = edge_attr[j]
|
|
158
|
+
if not G.has_edge(begin_idx, end_idx):
|
|
159
|
+
G.add_edge(begin_idx, end_idx, bond_type_idx=bond_type_idx,
|
|
160
|
+
bond_dir_idx=bond_dir_idx)
|
|
161
|
+
|
|
162
|
+
return G
|
|
163
|
+
|
|
164
|
+
def nx_to_graph_data_obj_simple(G):
|
|
165
|
+
"""
|
|
166
|
+
Converts nx graph to pytorch geometric Data object. Assume node indices
|
|
167
|
+
are numbered from 0 to num_nodes - 1. NB: Uses simplified atom and bond
|
|
168
|
+
features, and represent as indices. NB: possible issues with
|
|
169
|
+
recapitulating relative stereochemistry since the edges in the nx
|
|
170
|
+
object are unordered.
|
|
171
|
+
:param G: nx graph obj
|
|
172
|
+
:return: pytorch geometric Data object
|
|
173
|
+
"""
|
|
174
|
+
# atoms
|
|
175
|
+
num_atom_features = 2 # atom type, chirality tag
|
|
176
|
+
atom_features_list = []
|
|
177
|
+
for _, node in G.nodes(data=True):
|
|
178
|
+
atom_feature = [node['atom_num_idx'], node['chirality_tag_idx']]
|
|
179
|
+
atom_features_list.append(atom_feature)
|
|
180
|
+
x = torch.tensor(np.array(atom_features_list), dtype=torch.long)
|
|
181
|
+
|
|
182
|
+
# bonds
|
|
183
|
+
num_bond_features = 2 # bond type, bond direction
|
|
184
|
+
if len(G.edges()) > 0: # mol has bonds
|
|
185
|
+
edges_list = []
|
|
186
|
+
edge_features_list = []
|
|
187
|
+
for i, j, edge in G.edges(data=True):
|
|
188
|
+
edge_feature = [edge['bond_type_idx'], edge['bond_dir_idx']]
|
|
189
|
+
edges_list.append((i, j))
|
|
190
|
+
edge_features_list.append(edge_feature)
|
|
191
|
+
edges_list.append((j, i))
|
|
192
|
+
edge_features_list.append(edge_feature)
|
|
193
|
+
|
|
194
|
+
# data.edge_index: Graph connectivity in COO format with shape [2, num_edges]
|
|
195
|
+
edge_index = torch.tensor(np.array(edges_list).T, dtype=torch.long)
|
|
196
|
+
|
|
197
|
+
# data.edge_attr: Edge feature matrix with shape [num_edges, num_edge_features]
|
|
198
|
+
edge_attr = torch.tensor(np.array(edge_features_list),
|
|
199
|
+
dtype=torch.long)
|
|
200
|
+
else: # mol has no bonds
|
|
201
|
+
edge_index = torch.empty((2, 0), dtype=torch.long)
|
|
202
|
+
edge_attr = torch.empty((0, num_bond_features), dtype=torch.long)
|
|
203
|
+
|
|
204
|
+
data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr)
|
|
205
|
+
|
|
206
|
+
return data
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
from torch_geometric.nn import global_add_pool, global_mean_pool, global_max_pool
|
|
4
|
+
|
|
5
|
+
from ...nn import GNN_node, GNN_node_Virtualnode
|
|
6
|
+
from ...utils import init_weights
|
|
7
|
+
|
|
8
|
+
criterion = torch.nn.BCEWithLogitsLoss()
|
|
9
|
+
|
|
10
|
+
class GNN(nn.Module):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
num_layer,
|
|
14
|
+
hidden_size,
|
|
15
|
+
drop_ratio=0.5,
|
|
16
|
+
norm_layer="batch_norm",
|
|
17
|
+
encoder_type="gin-virtual",
|
|
18
|
+
readout="max"
|
|
19
|
+
):
|
|
20
|
+
super(GNN, self).__init__()
|
|
21
|
+
gnn_name = encoder_type.split("-")[0]
|
|
22
|
+
self.hidden_size = hidden_size
|
|
23
|
+
encoder_params = {
|
|
24
|
+
"num_layer": num_layer,
|
|
25
|
+
"hidden_size": hidden_size,
|
|
26
|
+
"JK": "last",
|
|
27
|
+
"drop_ratio": drop_ratio,
|
|
28
|
+
"residual": True,
|
|
29
|
+
"gnn_name": gnn_name,
|
|
30
|
+
"norm_layer": norm_layer
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Choose encoder type based on encoder_model
|
|
34
|
+
encoder_class = GNN_node_Virtualnode if "virtual" in encoder_type else GNN_node
|
|
35
|
+
self.graph_encoder = encoder_class(**encoder_params)
|
|
36
|
+
pooling_funcs = {
|
|
37
|
+
"sum": global_add_pool,
|
|
38
|
+
"mean": global_mean_pool,
|
|
39
|
+
"max": global_max_pool
|
|
40
|
+
}
|
|
41
|
+
self.pool = pooling_funcs.get(readout)
|
|
42
|
+
if self.pool is None:
|
|
43
|
+
raise ValueError(f"Invalid graph pooling type {readout}.")
|
|
44
|
+
|
|
45
|
+
def initialize_parameters(self, seed=None):
|
|
46
|
+
"""
|
|
47
|
+
Randomly initialize all model parameters using the init_weights function.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
seed (int, optional): Random seed for reproducibility. Defaults to None.
|
|
51
|
+
"""
|
|
52
|
+
if seed is not None:
|
|
53
|
+
torch.manual_seed(seed)
|
|
54
|
+
|
|
55
|
+
# Initialize the main components
|
|
56
|
+
init_weights(self.graph_encoder)
|
|
57
|
+
|
|
58
|
+
# Reset all parameters using PyTorch Geometric's reset function
|
|
59
|
+
def reset_parameters(module):
|
|
60
|
+
if hasattr(module, 'reset_parameters'):
|
|
61
|
+
module.reset_parameters()
|
|
62
|
+
elif hasattr(module, 'weight') and hasattr(module.weight, 'data'):
|
|
63
|
+
init_weights(module)
|
|
64
|
+
|
|
65
|
+
self.apply(reset_parameters)
|
|
66
|
+
|
|
67
|
+
def compute_loss(self, batched_data):
|
|
68
|
+
# sample negative edges
|
|
69
|
+
num_nodes = batched_data.num_nodes
|
|
70
|
+
num_edges = batched_data.num_edges
|
|
71
|
+
|
|
72
|
+
edge_set = set([str(batched_data.edge_index[0, i].cpu().item()) + "," + str(
|
|
73
|
+
batched_data.edge_index[1, i].cpu().item()) for i in range(batched_data.edge_index.shape[1])])
|
|
74
|
+
|
|
75
|
+
redandunt_sample = torch.randint(0, num_nodes, (2, 5 * num_edges))
|
|
76
|
+
sampled_ind = []
|
|
77
|
+
sampled_edge_set = set([])
|
|
78
|
+
for i in range(5 * num_edges):
|
|
79
|
+
node1 = redandunt_sample[0, i].cpu().item()
|
|
80
|
+
node2 = redandunt_sample[1, i].cpu().item()
|
|
81
|
+
edge_str = str(node1) + "," + str(node2)
|
|
82
|
+
if not edge_str in edge_set and not edge_str in sampled_edge_set and not node1 == node2:
|
|
83
|
+
sampled_edge_set.add(edge_str)
|
|
84
|
+
sampled_ind.append(i)
|
|
85
|
+
if len(sampled_ind) == num_edges / 2:
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
batched_data.negative_edge_index = redandunt_sample[:, sampled_ind]
|
|
89
|
+
|
|
90
|
+
# generate predictions
|
|
91
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
92
|
+
h_rep = self.pool(h_node, batched_data.batch)
|
|
93
|
+
|
|
94
|
+
positive_score = torch.sum(h_node[batched_data.edge_index[0, ::2]] * h_node[batched_data.edge_index[1, ::2]], dim = 1)
|
|
95
|
+
negative_score = torch.sum(h_node[batched_data.negative_edge_index[0]] * h_node[batched_data.negative_edge_index[1]], dim = 1)
|
|
96
|
+
|
|
97
|
+
loss_class = criterion(positive_score, torch.ones_like(positive_score)) + criterion(negative_score, torch.zeros_like(negative_score))
|
|
98
|
+
|
|
99
|
+
return loss_class
|
|
100
|
+
|
|
101
|
+
def forward(self, batched_data):
|
|
102
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
103
|
+
h_rep = self.pool(h_node, batched_data.batch)
|
|
104
|
+
return {"graph": h_rep, "node": h_node}
|