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,190 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import torch
|
|
3
|
+
import torch.nn as nn
|
|
4
|
+
# from torch_scatter import scatter_add
|
|
5
|
+
from .utils import scatter_add
|
|
6
|
+
|
|
7
|
+
from ...nn import GNN_node, GNN_node_Virtualnode, MLP
|
|
8
|
+
from ...utils import init_weights
|
|
9
|
+
|
|
10
|
+
class GREA(nn.Module):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
num_task,
|
|
14
|
+
num_layer,
|
|
15
|
+
gamma=0.4,
|
|
16
|
+
hidden_size=300,
|
|
17
|
+
gnn_type="gin-virtual",
|
|
18
|
+
drop_ratio=0.5,
|
|
19
|
+
norm_layer="batch_norm",
|
|
20
|
+
augmented_feature=['maccs', 'morgan']
|
|
21
|
+
):
|
|
22
|
+
super(GREA, self).__init__()
|
|
23
|
+
gnn_name = gnn_type.split("-")[0]
|
|
24
|
+
self.num_task = num_task
|
|
25
|
+
self.gamma = gamma
|
|
26
|
+
self.hidden_size = hidden_size
|
|
27
|
+
|
|
28
|
+
if "virtual" in gnn_type:
|
|
29
|
+
rationale_encoder = GNN_node_Virtualnode(
|
|
30
|
+
2, hidden_size, JK="last", drop_ratio=drop_ratio, residual=True, gnn_name=gnn_name
|
|
31
|
+
)
|
|
32
|
+
self.graph_encoder = GNN_node_Virtualnode(
|
|
33
|
+
num_layer,
|
|
34
|
+
hidden_size,
|
|
35
|
+
JK="last",
|
|
36
|
+
drop_ratio=drop_ratio,
|
|
37
|
+
residual=True,
|
|
38
|
+
gnn_name=gnn_name,
|
|
39
|
+
norm_layer=norm_layer,
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
rationale_encoder = GNN_node(
|
|
43
|
+
2, hidden_size, JK="last", drop_ratio=drop_ratio, residual=True, gnn_name=gnn_name
|
|
44
|
+
)
|
|
45
|
+
self.graph_encoder = GNN_node(
|
|
46
|
+
num_layer,
|
|
47
|
+
hidden_size,
|
|
48
|
+
JK="last",
|
|
49
|
+
drop_ratio=drop_ratio,
|
|
50
|
+
residual=True,
|
|
51
|
+
gnn_name=gnn_name,
|
|
52
|
+
norm_layer=norm_layer,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self.separator = Separator(
|
|
56
|
+
rationale_encoder=rationale_encoder,
|
|
57
|
+
gate_nn=torch.nn.Sequential(
|
|
58
|
+
torch.nn.Linear(hidden_size, 2 * hidden_size),
|
|
59
|
+
torch.nn.BatchNorm1d(2 * hidden_size),
|
|
60
|
+
torch.nn.ReLU(),
|
|
61
|
+
torch.nn.Dropout(),
|
|
62
|
+
torch.nn.Linear(2 * hidden_size, 1),
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
graph_dim = hidden_size
|
|
67
|
+
self.augmented_feature = augmented_feature
|
|
68
|
+
if augmented_feature:
|
|
69
|
+
if "morgan" in augmented_feature:
|
|
70
|
+
graph_dim += 1024
|
|
71
|
+
if "maccs" in augmented_feature:
|
|
72
|
+
graph_dim += 167
|
|
73
|
+
self.predictor = MLP(graph_dim, hidden_features=2 * hidden_size, out_features=num_task)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def initialize_parameters(self, seed=None):
|
|
77
|
+
"""
|
|
78
|
+
Randomly initialize all model parameters using the init_weights function.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
seed (int, optional): Random seed for reproducibility. Defaults to None.
|
|
82
|
+
"""
|
|
83
|
+
if seed is not None:
|
|
84
|
+
torch.manual_seed(seed)
|
|
85
|
+
|
|
86
|
+
# Initialize the main components
|
|
87
|
+
init_weights(self.graph_encoder)
|
|
88
|
+
init_weights(self.separator)
|
|
89
|
+
init_weights(self.predictor)
|
|
90
|
+
|
|
91
|
+
# Reset all parameters using PyTorch Geometric's reset function
|
|
92
|
+
def reset_parameters(module):
|
|
93
|
+
if hasattr(module, 'reset_parameters'):
|
|
94
|
+
module.reset_parameters()
|
|
95
|
+
elif hasattr(module, 'weight') and hasattr(module.weight, 'data'):
|
|
96
|
+
init_weights(module)
|
|
97
|
+
|
|
98
|
+
self.apply(reset_parameters)
|
|
99
|
+
|
|
100
|
+
def _augment_graph_features(self, batched_data, h_r, h_rep):
|
|
101
|
+
if self.augmented_feature:
|
|
102
|
+
if 'morgan' in self.augmented_feature:
|
|
103
|
+
morgan = batched_data.morgan.type_as(h_r)
|
|
104
|
+
h_r = torch.cat((h_r, morgan), dim=1)
|
|
105
|
+
morgans = morgan.repeat_interleave(batched_data.batch[-1]+1, dim=0)
|
|
106
|
+
h_rep = torch.cat((h_rep, morgans), dim=1)
|
|
107
|
+
if 'maccs' in self.augmented_feature:
|
|
108
|
+
maccs = batched_data.maccs.type_as(h_r)
|
|
109
|
+
h_r = torch.cat((h_r, maccs), dim=1)
|
|
110
|
+
maccses = maccs.repeat_interleave(batched_data.batch[-1]+1, dim=0)
|
|
111
|
+
h_rep = torch.cat((h_rep, maccses), dim=1)
|
|
112
|
+
return h_r, h_rep
|
|
113
|
+
|
|
114
|
+
def compute_loss(self, batched_data, criterion):
|
|
115
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
116
|
+
h_r, h_env, rationale_size, envir_size, _ = self.separator(batched_data, h_node)
|
|
117
|
+
h_rep = (h_r.unsqueeze(1) + h_env.unsqueeze(0)).view(-1, self.hidden_size)
|
|
118
|
+
h_r, h_rep = self._augment_graph_features(batched_data, h_r, h_rep)
|
|
119
|
+
pred_rem = self.predictor(h_r)
|
|
120
|
+
pred_rep = self.predictor(h_rep)
|
|
121
|
+
loss = torch.abs(
|
|
122
|
+
rationale_size / (rationale_size + envir_size)
|
|
123
|
+
- self.gamma * torch.ones_like(rationale_size)
|
|
124
|
+
).mean()
|
|
125
|
+
target = batched_data.y.to(torch.float32)
|
|
126
|
+
is_labeled = batched_data.y == batched_data.y
|
|
127
|
+
loss += criterion(pred_rem.to(torch.float32)[is_labeled], target[is_labeled]).mean()
|
|
128
|
+
target_rep = batched_data.y.to(torch.float32).repeat_interleave(
|
|
129
|
+
batched_data.batch[-1] + 1, dim=0
|
|
130
|
+
)
|
|
131
|
+
is_labeled_rep = target_rep == target_rep
|
|
132
|
+
loss += criterion(pred_rep.to(torch.float32)[is_labeled_rep], target_rep[is_labeled_rep]).mean()
|
|
133
|
+
return loss
|
|
134
|
+
|
|
135
|
+
def forward(self, batched_data):
|
|
136
|
+
h_node, _ = self.graph_encoder(batched_data)
|
|
137
|
+
h_r, h_env, _, _, node_score = self.separator(batched_data, h_node)
|
|
138
|
+
h_rep = (h_r.unsqueeze(1) + h_env.unsqueeze(0)).view(-1, self.hidden_size)
|
|
139
|
+
h_r, h_rep = self._augment_graph_features(batched_data, h_r, h_rep)
|
|
140
|
+
prediction = self.predictor(h_r)
|
|
141
|
+
pred_rep = self.predictor(h_rep).view(h_r.size(0), -1)
|
|
142
|
+
if pred_rep.size(1) > 1:
|
|
143
|
+
variance = pred_rep.var(dim=-1, keepdim=True)
|
|
144
|
+
else:
|
|
145
|
+
variance = torch.zeros_like(pred_rep)
|
|
146
|
+
num_graphs = batched_data.batch.max().item() + 1
|
|
147
|
+
score_by_graph = [node_score[batched_data.batch == i].view(-1).tolist() for i in range(num_graphs)]
|
|
148
|
+
return {"prediction": prediction, "variance": variance, "score": score_by_graph, "representation": h_r}
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def _disable_batchnorm_tracking(model):
|
|
152
|
+
def fn(module):
|
|
153
|
+
if isinstance(module, torch.nn.modules.batchnorm._BatchNorm):
|
|
154
|
+
module.track_running_stats = False
|
|
155
|
+
|
|
156
|
+
model.apply(fn)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _enable_batchnorm_tracking(model):
|
|
160
|
+
def fn(module):
|
|
161
|
+
if isinstance(module, torch.nn.modules.batchnorm._BatchNorm):
|
|
162
|
+
module.track_running_stats = True
|
|
163
|
+
|
|
164
|
+
model.apply(fn)
|
|
165
|
+
|
|
166
|
+
class Separator(torch.nn.Module):
|
|
167
|
+
def __init__(self, rationale_encoder, gate_nn, nn=None):
|
|
168
|
+
super(Separator, self).__init__()
|
|
169
|
+
self.rationale_encoder = rationale_encoder
|
|
170
|
+
self.gate_nn = gate_nn
|
|
171
|
+
self.nn = nn
|
|
172
|
+
|
|
173
|
+
def forward(self, batched_data, h_node, size=None):
|
|
174
|
+
x, _ = self.rationale_encoder(batched_data)
|
|
175
|
+
batch = batched_data.batch
|
|
176
|
+
x = x.unsqueeze(-1) if x.dim() == 1 else x
|
|
177
|
+
size = batch[-1].item() + 1 if size is None else size
|
|
178
|
+
|
|
179
|
+
gate = self.gate_nn(x).view(-1, 1)
|
|
180
|
+
h_node = self.nn(h_node) if self.nn is not None else h_node
|
|
181
|
+
assert gate.dim() == h_node.dim() and gate.size(0) == h_node.size(0)
|
|
182
|
+
gate = torch.sigmoid(gate)
|
|
183
|
+
|
|
184
|
+
h_out = scatter_add(gate * h_node, batch, dim=0, dim_size=size)
|
|
185
|
+
c_out = scatter_add((1 - gate) * h_node, batch, dim=0, dim_size=size)
|
|
186
|
+
|
|
187
|
+
rationale_size = scatter_add(gate, batch, dim=0, dim_size=size)
|
|
188
|
+
envir_size = scatter_add((1 - gate), batch, dim=0, dim_size=size)
|
|
189
|
+
|
|
190
|
+
return h_out, c_out, rationale_size + 1e-8, envir_size + 1e-8, gate
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import warnings
|
|
3
|
+
from tqdm import tqdm
|
|
4
|
+
from typing import Optional, Union, Dict, Any, List, Type
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from torch_geometric.loader import DataLoader
|
|
9
|
+
|
|
10
|
+
from .model import GREA
|
|
11
|
+
from ..gnn.modeling_gnn import GNNMolecularPredictor
|
|
12
|
+
from ...utils.search import (
|
|
13
|
+
ParameterSpec,
|
|
14
|
+
ParameterType,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class GREAMolecularPredictor(GNNMolecularPredictor):
|
|
19
|
+
"""This predictor implements a Graph Rationalization model called GREA.
|
|
20
|
+
|
|
21
|
+
The full name of GREA is Graph Rationalization with Environment-based Augmentations. During model training, it learns the rationales (explainable subgraphs) and use them for molecular property prediction tasks.
|
|
22
|
+
|
|
23
|
+
References
|
|
24
|
+
----------
|
|
25
|
+
- Graph Rationalization with Environment-based Augmentations.
|
|
26
|
+
https://dl.acm.org/doi/10.1145/3534678.3539347
|
|
27
|
+
- Code: https://github.com/liugangcode/GREA
|
|
28
|
+
|
|
29
|
+
:param gamma: GREA-specific parameter. Default is 0.4.
|
|
30
|
+
:type gamma: float
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# GREA-specific parameter
|
|
34
|
+
gamma: float = 0.4
|
|
35
|
+
# Override parent defaults
|
|
36
|
+
model_name: str = "GREAMolecularPredictor"
|
|
37
|
+
model_class: Type[GREA] = field(default=GREA, init=False)
|
|
38
|
+
|
|
39
|
+
def __post_init__(self):
|
|
40
|
+
super().__post_init__()
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _get_param_names() -> List[str]:
|
|
44
|
+
return ["gamma"] + GNNMolecularPredictor._get_param_names()
|
|
45
|
+
|
|
46
|
+
def _get_default_search_space(self):
|
|
47
|
+
search_space = super()._get_default_search_space().copy()
|
|
48
|
+
search_space["gamma"] = ParameterSpec(ParameterType.FLOAT, (0.1, 0.9))
|
|
49
|
+
return search_space
|
|
50
|
+
|
|
51
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
52
|
+
base_params = super()._get_model_params(checkpoint)
|
|
53
|
+
if checkpoint and "hyperparameters" in checkpoint:
|
|
54
|
+
base_params["gamma"] = checkpoint["hyperparameters"].get("gamma", self.gamma)
|
|
55
|
+
else:
|
|
56
|
+
base_params["gamma"] = self.gamma
|
|
57
|
+
base_params.pop("graph_pooling", None)
|
|
58
|
+
return base_params
|
|
59
|
+
|
|
60
|
+
def predict(self, X: List[str]) -> Dict[str, Union[np.ndarray, List[List]]]:
|
|
61
|
+
"""Make predictions using the fitted model.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
X : List[str]
|
|
66
|
+
List of SMILES strings to make predictions for
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
Dict[str, np.ndarray]
|
|
71
|
+
Dictionary containing:
|
|
72
|
+
- 'prediction': Model predictions (shape: [n_samples, n_tasks])
|
|
73
|
+
- 'variance': Prediction variances (shape: [n_samples, n_tasks])
|
|
74
|
+
- 'node_importance': A nested list where the outer list has length n_samples and each inner list has length n_nodes for that molecule
|
|
75
|
+
"""
|
|
76
|
+
self._check_is_fitted()
|
|
77
|
+
|
|
78
|
+
# Convert to PyTorch Geometric format and create loader
|
|
79
|
+
X, _ = self._validate_inputs(X)
|
|
80
|
+
dataset = self._convert_to_pytorch_data(X)
|
|
81
|
+
loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
|
|
82
|
+
|
|
83
|
+
# Make predictions
|
|
84
|
+
self.model = self.model.to(self.device)
|
|
85
|
+
self.model.eval()
|
|
86
|
+
predictions = []
|
|
87
|
+
variances = []
|
|
88
|
+
node_scores = []
|
|
89
|
+
with torch.no_grad():
|
|
90
|
+
for batch in tqdm(loader, disable=not self.verbose):
|
|
91
|
+
batch = batch.to(self.device)
|
|
92
|
+
out = self.model(batch)
|
|
93
|
+
predictions.append(out["prediction"].cpu().numpy())
|
|
94
|
+
variances.append(out["variance"].cpu().numpy())
|
|
95
|
+
node_scores.extend(out["score"])
|
|
96
|
+
|
|
97
|
+
if predictions and variances:
|
|
98
|
+
return {
|
|
99
|
+
"prediction": np.concatenate(predictions, axis=0),
|
|
100
|
+
"variance": np.concatenate(variances, axis=0),
|
|
101
|
+
"node_importance": node_scores,
|
|
102
|
+
}
|
|
103
|
+
else:
|
|
104
|
+
warnings.warn(
|
|
105
|
+
"No valid predictions could be made from the input data. Returning empty results."
|
|
106
|
+
)
|
|
107
|
+
return {"prediction": np.array([]), "variance": np.array([]), "node_importance": np.array([])}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# allowable multiple choice node and edge features
|
|
2
|
+
allowable_features = {
|
|
3
|
+
'possible_atomic_num_list' : list(range(1, 119)) + ['misc'],
|
|
4
|
+
'possible_chirality_list' : [
|
|
5
|
+
'CHI_UNSPECIFIED',
|
|
6
|
+
'CHI_TETRAHEDRAL_CW',
|
|
7
|
+
'CHI_TETRAHEDRAL_CCW',
|
|
8
|
+
'CHI_OTHER',
|
|
9
|
+
'misc'
|
|
10
|
+
],
|
|
11
|
+
'possible_degree_list' : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 'misc'],
|
|
12
|
+
'possible_formal_charge_list' : [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 'misc'],
|
|
13
|
+
'possible_numH_list' : [0, 1, 2, 3, 4, 5, 6, 7, 8, 'misc'],
|
|
14
|
+
'possible_number_radical_e_list': [0, 1, 2, 3, 4, 'misc'],
|
|
15
|
+
'possible_hybridization_list' : [
|
|
16
|
+
'SP', 'SP2', 'SP3', 'SP3D', 'SP3D2', 'misc'
|
|
17
|
+
],
|
|
18
|
+
'possible_is_aromatic_list': [False, True],
|
|
19
|
+
'possible_is_in_ring_list': [False, True],
|
|
20
|
+
'possible_bond_type_list' : [
|
|
21
|
+
'SINGLE',
|
|
22
|
+
'DOUBLE',
|
|
23
|
+
'TRIPLE',
|
|
24
|
+
'AROMATIC',
|
|
25
|
+
'misc'
|
|
26
|
+
],
|
|
27
|
+
'possible_bond_stereo_list': [
|
|
28
|
+
'STEREONONE',
|
|
29
|
+
'STEREOZ',
|
|
30
|
+
'STEREOE',
|
|
31
|
+
'STEREOCIS',
|
|
32
|
+
'STEREOTRANS',
|
|
33
|
+
'STEREOANY',
|
|
34
|
+
],
|
|
35
|
+
'possible_is_conjugated_list': [False, True],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def safe_index(l, e):
|
|
39
|
+
"""
|
|
40
|
+
Return index of element e in list l. If e is not present, return the last index
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
return l.index(e)
|
|
44
|
+
except:
|
|
45
|
+
return len(l) - 1
|
|
46
|
+
# # miscellaneous case
|
|
47
|
+
# i = safe_index(allowable_features['possible_atomic_num_list'], 'asdf')
|
|
48
|
+
# assert allowable_features['possible_atomic_num_list'][i] == 'misc'
|
|
49
|
+
# # normal case
|
|
50
|
+
# i = safe_index(allowable_features['possible_atomic_num_list'], 2)
|
|
51
|
+
# assert allowable_features['possible_atomic_num_list'][i] == 2
|
|
52
|
+
|
|
53
|
+
def atom_to_feature_vector(atom):
|
|
54
|
+
"""
|
|
55
|
+
Converts rdkit atom object to feature list of indices
|
|
56
|
+
:param mol: rdkit atom object
|
|
57
|
+
:return: list
|
|
58
|
+
"""
|
|
59
|
+
atom_feature = [
|
|
60
|
+
safe_index(allowable_features['possible_atomic_num_list'], atom.GetAtomicNum()),
|
|
61
|
+
safe_index(allowable_features['possible_chirality_list'], str(atom.GetChiralTag())),
|
|
62
|
+
safe_index(allowable_features['possible_degree_list'], atom.GetTotalDegree()),
|
|
63
|
+
safe_index(allowable_features['possible_formal_charge_list'], atom.GetFormalCharge()),
|
|
64
|
+
safe_index(allowable_features['possible_numH_list'], atom.GetTotalNumHs()),
|
|
65
|
+
safe_index(allowable_features['possible_number_radical_e_list'], atom.GetNumRadicalElectrons()),
|
|
66
|
+
safe_index(allowable_features['possible_hybridization_list'], str(atom.GetHybridization())),
|
|
67
|
+
allowable_features['possible_is_aromatic_list'].index(atom.GetIsAromatic()),
|
|
68
|
+
allowable_features['possible_is_in_ring_list'].index(atom.IsInRing()),
|
|
69
|
+
]
|
|
70
|
+
return atom_feature
|
|
71
|
+
# from rdkit import Chem
|
|
72
|
+
# mol = Chem.MolFromSmiles('Cl[C@H](/C=C/C)Br')
|
|
73
|
+
# atom = mol.GetAtomWithIdx(1) # chiral carbon
|
|
74
|
+
# atom_feature = atom_to_feature_vector(atom)
|
|
75
|
+
# assert atom_feature == [5, 2, 4, 5, 1, 0, 2, 0, 0]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_atom_feature_dims():
|
|
79
|
+
return list(map(len, [
|
|
80
|
+
allowable_features['possible_atomic_num_list'],
|
|
81
|
+
allowable_features['possible_chirality_list'],
|
|
82
|
+
allowable_features['possible_degree_list'],
|
|
83
|
+
allowable_features['possible_formal_charge_list'],
|
|
84
|
+
allowable_features['possible_numH_list'],
|
|
85
|
+
allowable_features['possible_number_radical_e_list'],
|
|
86
|
+
allowable_features['possible_hybridization_list'],
|
|
87
|
+
allowable_features['possible_is_aromatic_list'],
|
|
88
|
+
allowable_features['possible_is_in_ring_list']
|
|
89
|
+
]))
|
|
90
|
+
|
|
91
|
+
def bond_to_feature_vector(bond):
|
|
92
|
+
"""
|
|
93
|
+
Converts rdkit bond object to feature list of indices
|
|
94
|
+
:param mol: rdkit bond object
|
|
95
|
+
:return: list
|
|
96
|
+
"""
|
|
97
|
+
bond_feature = [
|
|
98
|
+
safe_index(allowable_features['possible_bond_type_list'], str(bond.GetBondType())),
|
|
99
|
+
allowable_features['possible_bond_stereo_list'].index(str(bond.GetStereo())),
|
|
100
|
+
allowable_features['possible_is_conjugated_list'].index(bond.GetIsConjugated()),
|
|
101
|
+
]
|
|
102
|
+
return bond_feature
|
|
103
|
+
# uses same molecule as atom_to_feature_vector test
|
|
104
|
+
# bond = mol.GetBondWithIdx(2) # double bond with stereochem
|
|
105
|
+
# bond_feature = bond_to_feature_vector(bond)
|
|
106
|
+
# assert bond_feature == [1, 2, 0]
|
|
107
|
+
|
|
108
|
+
def get_bond_feature_dims():
|
|
109
|
+
return list(map(len, [
|
|
110
|
+
allowable_features['possible_bond_type_list'],
|
|
111
|
+
allowable_features['possible_bond_stereo_list'],
|
|
112
|
+
allowable_features['possible_is_conjugated_list']
|
|
113
|
+
]))
|
|
114
|
+
|
|
115
|
+
def atom_feature_vector_to_dict(atom_feature):
|
|
116
|
+
[atomic_num_idx,
|
|
117
|
+
chirality_idx,
|
|
118
|
+
degree_idx,
|
|
119
|
+
formal_charge_idx,
|
|
120
|
+
num_h_idx,
|
|
121
|
+
number_radical_e_idx,
|
|
122
|
+
hybridization_idx,
|
|
123
|
+
is_aromatic_idx,
|
|
124
|
+
is_in_ring_idx] = atom_feature
|
|
125
|
+
|
|
126
|
+
feature_dict = {
|
|
127
|
+
'atomic_num': allowable_features['possible_atomic_num_list'][atomic_num_idx],
|
|
128
|
+
'chirality': allowable_features['possible_chirality_list'][chirality_idx],
|
|
129
|
+
'degree': allowable_features['possible_degree_list'][degree_idx],
|
|
130
|
+
'formal_charge': allowable_features['possible_formal_charge_list'][formal_charge_idx],
|
|
131
|
+
'num_h': allowable_features['possible_numH_list'][num_h_idx],
|
|
132
|
+
'num_rad_e': allowable_features['possible_number_radical_e_list'][number_radical_e_idx],
|
|
133
|
+
'hybridization': allowable_features['possible_hybridization_list'][hybridization_idx],
|
|
134
|
+
'is_aromatic': allowable_features['possible_is_aromatic_list'][is_aromatic_idx],
|
|
135
|
+
'is_in_ring': allowable_features['possible_is_in_ring_list'][is_in_ring_idx]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return feature_dict
|
|
139
|
+
# # uses same atom_feature as atom_to_feature_vector test
|
|
140
|
+
# atom_feature_dict = atom_feature_vector_to_dict(atom_feature)
|
|
141
|
+
# assert atom_feature_dict['atomic_num'] == 6
|
|
142
|
+
# assert atom_feature_dict['chirality'] == 'CHI_TETRAHEDRAL_CCW'
|
|
143
|
+
# assert atom_feature_dict['degree'] == 4
|
|
144
|
+
# assert atom_feature_dict['formal_charge'] == 0
|
|
145
|
+
# assert atom_feature_dict['num_h'] == 1
|
|
146
|
+
# assert atom_feature_dict['num_rad_e'] == 0
|
|
147
|
+
# assert atom_feature_dict['hybridization'] == 'SP3'
|
|
148
|
+
# assert atom_feature_dict['is_aromatic'] == False
|
|
149
|
+
# assert atom_feature_dict['is_in_ring'] == False
|
|
150
|
+
|
|
151
|
+
def bond_feature_vector_to_dict(bond_feature):
|
|
152
|
+
[bond_type_idx,
|
|
153
|
+
bond_stereo_idx,
|
|
154
|
+
is_conjugated_idx] = bond_feature
|
|
155
|
+
|
|
156
|
+
feature_dict = {
|
|
157
|
+
'bond_type': allowable_features['possible_bond_type_list'][bond_type_idx],
|
|
158
|
+
'bond_stereo': allowable_features['possible_bond_stereo_list'][bond_stereo_idx],
|
|
159
|
+
'is_conjugated': allowable_features['possible_is_conjugated_list'][is_conjugated_idx]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return feature_dict
|
|
163
|
+
# # uses same bond as bond_to_feature_vector test
|
|
164
|
+
# bond_feature_dict = bond_feature_vector_to_dict(bond_feature)
|
|
165
|
+
# assert bond_feature_dict['bond_type'] == 'DOUBLE'
|
|
166
|
+
# assert bond_feature_dict['bond_stereo'] == 'STEREOE'
|
|
167
|
+
# assert bond_feature_dict['is_conjugated'] == False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
import numpy as np
|
|
171
|
+
from rdkit import Chem
|
|
172
|
+
|
|
173
|
+
def smiles_to_graph(smiles_string, properties):
|
|
174
|
+
"""
|
|
175
|
+
Converts SMILES string to graph Data object
|
|
176
|
+
:input: SMILES string (str)
|
|
177
|
+
:return: graph object
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
mol = Chem.MolFromSmiles(smiles_string)
|
|
181
|
+
# atoms
|
|
182
|
+
atom_features_list = []
|
|
183
|
+
# atom_label = []
|
|
184
|
+
for atom in mol.GetAtoms():
|
|
185
|
+
atom_features_list.append(atom_to_feature_vector(atom))
|
|
186
|
+
# atom_label.append(atom.GetSymbol())
|
|
187
|
+
|
|
188
|
+
x = np.array(atom_features_list, dtype=np.int64)
|
|
189
|
+
|
|
190
|
+
# bonds
|
|
191
|
+
num_bond_features = 3 # bond type, bond stereo, is_conjugated
|
|
192
|
+
if len(mol.GetBonds()) > 0: # mol has bonds
|
|
193
|
+
edges_list = []
|
|
194
|
+
edge_features_list = []
|
|
195
|
+
for bond in mol.GetBonds():
|
|
196
|
+
i = bond.GetBeginAtomIdx()
|
|
197
|
+
j = bond.GetEndAtomIdx()
|
|
198
|
+
|
|
199
|
+
edge_feature = bond_to_feature_vector(bond)
|
|
200
|
+
# add edges in both directions
|
|
201
|
+
edges_list.append((i, j))
|
|
202
|
+
edge_features_list.append(edge_feature)
|
|
203
|
+
edges_list.append((j, i))
|
|
204
|
+
edge_features_list.append(edge_feature)
|
|
205
|
+
|
|
206
|
+
# data.edge_index: Graph connectivity in COO format with shape [2, num_edges]
|
|
207
|
+
edge_index = np.array(edges_list, dtype=np.int64).T
|
|
208
|
+
|
|
209
|
+
# data.edge_attr: Edge feature matrix with shape [num_edges, num_edge_features]
|
|
210
|
+
edge_attr = np.array(edge_features_list, dtype=np.int64)
|
|
211
|
+
|
|
212
|
+
else: # mol has no bonds
|
|
213
|
+
edge_index = np.empty((2, 0), dtype=np.int64)
|
|
214
|
+
edge_attr = np.empty((0, num_bond_features), dtype=np.int64)
|
|
215
|
+
|
|
216
|
+
graph = dict()
|
|
217
|
+
graph["edge_index"] = edge_index
|
|
218
|
+
graph["edge_feat"] = edge_attr
|
|
219
|
+
graph["node_feat"] = x
|
|
220
|
+
graph["num_nodes"] = len(x)
|
|
221
|
+
if properties is None:
|
|
222
|
+
y = np.full(1, np.nan, dtype=np.float32)
|
|
223
|
+
graph['y'] = y.reshape(1, -1)
|
|
224
|
+
else:
|
|
225
|
+
graph['y'] = np.array(properties, dtype=np.float32).reshape(1, -1)
|
|
226
|
+
|
|
227
|
+
return graph
|
|
228
|
+
except:
|
|
229
|
+
return None
|