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,581 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from tqdm import tqdm
|
|
3
|
+
from typing import Optional, Union, Dict, Any, Tuple, List, Type
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
import torch.nn.functional as F
|
|
8
|
+
from torch_geometric.loader import DataLoader
|
|
9
|
+
from torch_geometric.data import Data
|
|
10
|
+
|
|
11
|
+
from .transformer import Transformer
|
|
12
|
+
from .utils import PlaceHolder, to_dense, compute_dataset_info
|
|
13
|
+
from .diffusion import NoiseScheduleDiscrete, MarginalTransition, sample_discrete_features, sample_discrete_feature_noise, reverse_diffusion
|
|
14
|
+
|
|
15
|
+
from ...base import BaseMolecularGenerator
|
|
16
|
+
from ...utils import graph_from_smiles, graph_to_smiles
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class GraphDITMolecularGenerator(BaseMolecularGenerator):
|
|
20
|
+
"""
|
|
21
|
+
This generator implements the graph diffusion transformer for (multi-conditional and unconditional) molecular generation.
|
|
22
|
+
|
|
23
|
+
References
|
|
24
|
+
----------
|
|
25
|
+
- Graph Diffusion Transformers for Multi-Conditional Molecular Generation. NeurIPS 2024.
|
|
26
|
+
https://openreview.net/forum?id=cfrDLD1wfO
|
|
27
|
+
- Implementation: https://github.com/liugangcode/Graph-DiT
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
num_layer : int, default=6
|
|
32
|
+
Number of transformer layers
|
|
33
|
+
hidden_size : int, default=1152
|
|
34
|
+
Dimension of hidden layers
|
|
35
|
+
dropout : float, default=0.0
|
|
36
|
+
Dropout rate for transformer layers
|
|
37
|
+
drop_condition : float, default=0.0
|
|
38
|
+
Dropout rate for condition embedding
|
|
39
|
+
num_head : int, default=16
|
|
40
|
+
Number of attention heads in transformer
|
|
41
|
+
mlp_ratio : float, default=4
|
|
42
|
+
Ratio of MLP hidden dimension to transformer hidden dimension
|
|
43
|
+
task_type : List[str], default=[]
|
|
44
|
+
List specifying type of each task ('regression' or 'classification')
|
|
45
|
+
timesteps : int, default=500
|
|
46
|
+
Number of diffusion timesteps
|
|
47
|
+
batch_size : int, default=128
|
|
48
|
+
Batch size for training
|
|
49
|
+
epochs : int, default=10000
|
|
50
|
+
Number of training epochs
|
|
51
|
+
learning_rate : float, default=0.0002
|
|
52
|
+
Learning rate for optimization
|
|
53
|
+
grad_clip_value : Optional[float], default=None
|
|
54
|
+
Value for gradient clipping (None = no clipping)
|
|
55
|
+
weight_decay : float, default=0.0
|
|
56
|
+
Weight decay for optimization
|
|
57
|
+
lw_X : float, default=1
|
|
58
|
+
Loss weight for node reconstruction
|
|
59
|
+
lw_E : float, default=5
|
|
60
|
+
Loss weight for edge reconstruction
|
|
61
|
+
guide_scale : float, default=2.0
|
|
62
|
+
Scale factor for classifier-free guidance during sampling
|
|
63
|
+
use_lr_scheduler : bool, default=False
|
|
64
|
+
Whether to use learning rate scheduler
|
|
65
|
+
scheduler_factor : float, default=0.5
|
|
66
|
+
Factor by which to reduce learning rate on plateau
|
|
67
|
+
scheduler_patience : int, default=5
|
|
68
|
+
Number of epochs with no improvement after which learning rate will be reduced
|
|
69
|
+
verbose : bool, default=False
|
|
70
|
+
Whether to display progress bars and logs
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
# Model parameters
|
|
74
|
+
num_layer: int = 6
|
|
75
|
+
hidden_size: int = 1152
|
|
76
|
+
dropout: float = 0.
|
|
77
|
+
drop_condition: float = 0.
|
|
78
|
+
num_head: int = 16
|
|
79
|
+
mlp_ratio: float = 4
|
|
80
|
+
task_type: List[str] = field(default_factory=list)
|
|
81
|
+
|
|
82
|
+
# Diffusion parameters
|
|
83
|
+
timesteps: int = 500
|
|
84
|
+
|
|
85
|
+
# Training parameters
|
|
86
|
+
batch_size: int = 128
|
|
87
|
+
epochs: int = 10000
|
|
88
|
+
learning_rate: float = 0.0002
|
|
89
|
+
grad_clip_value: Optional[float] = None
|
|
90
|
+
weight_decay: float = 0.0
|
|
91
|
+
lw_X: float = 1
|
|
92
|
+
lw_E: float = 5
|
|
93
|
+
# Sampling parameters
|
|
94
|
+
guide_scale: float = 2.
|
|
95
|
+
# Scheduler parameters
|
|
96
|
+
use_lr_scheduler: bool = False
|
|
97
|
+
scheduler_factor: float = 0.5
|
|
98
|
+
scheduler_patience: int = 5
|
|
99
|
+
|
|
100
|
+
verbose: bool = False
|
|
101
|
+
|
|
102
|
+
# Attributes
|
|
103
|
+
model_name: str = "GraphDITMolecularGenerator"
|
|
104
|
+
fitting_loss: List[float] = field(default_factory=list, init=False)
|
|
105
|
+
fitting_epoch: int = field(default=0, init=False)
|
|
106
|
+
dataset_info: Dict[str, Any] = field(default_factory=dict, init=False)
|
|
107
|
+
model_class: Type[Transformer] = field(default=Transformer, init=False)
|
|
108
|
+
|
|
109
|
+
def __post_init__(self):
|
|
110
|
+
"""Initialize the model after dataclass initialization."""
|
|
111
|
+
super().__post_init__()
|
|
112
|
+
self.max_node = None
|
|
113
|
+
self.input_dim_X = None
|
|
114
|
+
self.input_dim_E = None
|
|
115
|
+
self.input_dim_y = len(self.task_type)
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _get_param_names() -> List[str]:
|
|
119
|
+
"""Get parameter names for the estimator.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
List[str]
|
|
124
|
+
List of parameter names that can be used for model configuration.
|
|
125
|
+
"""
|
|
126
|
+
return [
|
|
127
|
+
# Model Hyperparameters
|
|
128
|
+
"max_node", "hidden_size", "num_layer", "num_head",
|
|
129
|
+
"mlp_ratio", "dropout", "drop_condition", "input_dim_X", "input_dim_E", "input_dim_y",
|
|
130
|
+
"task_type",
|
|
131
|
+
# Diffusion parameters
|
|
132
|
+
"timesteps", "dataset_info",
|
|
133
|
+
# Training Parameters
|
|
134
|
+
"batch_size", "epochs", "learning_rate", "grad_clip_value",
|
|
135
|
+
"weight_decay", "lw_X", "lw_E",
|
|
136
|
+
# Scheduler Parameters
|
|
137
|
+
"use_lr_scheduler", "scheduler_factor", "scheduler_patience",
|
|
138
|
+
# Sampling Parameters
|
|
139
|
+
"guide_scale",
|
|
140
|
+
# Other Parameters
|
|
141
|
+
"fitting_epoch", "fitting_loss", "device", "verbose", "model_name"
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
|
|
145
|
+
params = ["max_node", "hidden_size", "num_layer", "num_head", "mlp_ratio",
|
|
146
|
+
"dropout", "drop_condition", "input_dim_X", "input_dim_E", "input_dim_y", "task_type"]
|
|
147
|
+
|
|
148
|
+
if checkpoint is not None:
|
|
149
|
+
if "hyperparameters" not in checkpoint:
|
|
150
|
+
raise ValueError("Checkpoint missing 'hyperparameters' key")
|
|
151
|
+
return {k: checkpoint["hyperparameters"][k] for k in params}
|
|
152
|
+
|
|
153
|
+
return {k: getattr(self, k) for k in params}
|
|
154
|
+
|
|
155
|
+
def _convert_to_pytorch_data(self, X, y=None):
|
|
156
|
+
"""Convert numpy arrays to PyTorch Geometric data format.
|
|
157
|
+
"""
|
|
158
|
+
if self.verbose:
|
|
159
|
+
iterator = tqdm(enumerate(X), desc="Converting molecules to graphs", total=len(X))
|
|
160
|
+
else:
|
|
161
|
+
iterator = enumerate(X)
|
|
162
|
+
|
|
163
|
+
pyg_graph_list = []
|
|
164
|
+
for idx, smiles_or_mol in iterator:
|
|
165
|
+
if y is not None:
|
|
166
|
+
properties = y[idx]
|
|
167
|
+
else:
|
|
168
|
+
properties = None
|
|
169
|
+
graph = graph_from_smiles(smiles_or_mol, properties)
|
|
170
|
+
g = Data()
|
|
171
|
+
|
|
172
|
+
# No H, first heavy atom has type 0
|
|
173
|
+
node_type = torch.from_numpy(graph['node_feat'][:, 0] - 1)
|
|
174
|
+
|
|
175
|
+
# Filter out invalid node types (< 0)
|
|
176
|
+
valid_mask = node_type >= 0
|
|
177
|
+
if not valid_mask.all():
|
|
178
|
+
# Get valid nodes and adjust edge indices
|
|
179
|
+
valid_indices = torch.where(valid_mask)[0]
|
|
180
|
+
index_map = -torch.ones(node_type.size(0), dtype=torch.long)
|
|
181
|
+
index_map[valid_indices] = torch.arange(valid_indices.size(0))
|
|
182
|
+
|
|
183
|
+
# Filter edges that connect to invalid nodes
|
|
184
|
+
edge_index = torch.from_numpy(graph["edge_index"])
|
|
185
|
+
valid_edges_mask = valid_mask[edge_index[0]] & valid_mask[edge_index[1]]
|
|
186
|
+
valid_edge_index = edge_index[:, valid_edges_mask]
|
|
187
|
+
|
|
188
|
+
# Remap edge indices to account for removed nodes
|
|
189
|
+
remapped_edge_index = index_map[valid_edge_index]
|
|
190
|
+
|
|
191
|
+
# Filter edge attributes
|
|
192
|
+
edge_attr = torch.from_numpy(graph["edge_feat"])[:, 0] + 1
|
|
193
|
+
valid_edge_attr = edge_attr[valid_edges_mask]
|
|
194
|
+
|
|
195
|
+
# Update node and edge data
|
|
196
|
+
node_type = node_type[valid_mask]
|
|
197
|
+
g.edge_index = remapped_edge_index
|
|
198
|
+
g.edge_attr = valid_edge_attr.long().squeeze(-1)
|
|
199
|
+
else:
|
|
200
|
+
# No invalid nodes, proceed normally
|
|
201
|
+
g.edge_index = torch.from_numpy(graph["edge_index"])
|
|
202
|
+
edge_attr = torch.from_numpy(graph["edge_feat"])[:, 0] + 1
|
|
203
|
+
g.edge_attr = edge_attr.long().squeeze(-1)
|
|
204
|
+
|
|
205
|
+
# * is encoded as "misc" which is 119 - 1 and should be 117
|
|
206
|
+
node_type[node_type == 118] = 117
|
|
207
|
+
g.x = node_type.long().squeeze(-1)
|
|
208
|
+
del graph["node_feat"]
|
|
209
|
+
del graph["edge_index"]
|
|
210
|
+
del graph["edge_feat"]
|
|
211
|
+
|
|
212
|
+
g.y = torch.from_numpy(graph["y"])
|
|
213
|
+
del graph["y"]
|
|
214
|
+
|
|
215
|
+
pyg_graph_list.append(g)
|
|
216
|
+
|
|
217
|
+
return pyg_graph_list
|
|
218
|
+
|
|
219
|
+
def _setup_diffusion_params(self, X: Union[List, Dict]) -> None:
|
|
220
|
+
# Extract dataset info from X if it's a dict (from checkpoint), otherwise compute it
|
|
221
|
+
if isinstance(X, dict):
|
|
222
|
+
dataset_info = X["hyperparameters"]["dataset_info"]
|
|
223
|
+
timesteps = X["hyperparameters"]["timesteps"]
|
|
224
|
+
max_node = X["hyperparameters"]["max_node"]
|
|
225
|
+
else:
|
|
226
|
+
assert isinstance(X, list)
|
|
227
|
+
dataset_info = compute_dataset_info(X)
|
|
228
|
+
timesteps = self.timesteps
|
|
229
|
+
max_node = dataset_info["max_node"]
|
|
230
|
+
|
|
231
|
+
self.input_dim_X = dataset_info["x_margins"].shape[0]
|
|
232
|
+
self.input_dim_E = dataset_info["e_margins"].shape[0]
|
|
233
|
+
self.dataset_info = dataset_info
|
|
234
|
+
self.timesteps = timesteps
|
|
235
|
+
self.max_node = max_node
|
|
236
|
+
|
|
237
|
+
x_limit = dataset_info["x_margins"].to(self.device)
|
|
238
|
+
e_limit = dataset_info["e_margins"].to(self.device)
|
|
239
|
+
xe_conditions = dataset_info["xe_conditions"].to(self.device)
|
|
240
|
+
ex_conditions = dataset_info["ex_conditions"].to(self.device)
|
|
241
|
+
|
|
242
|
+
self.transition_model = MarginalTransition(x_limit, e_limit, xe_conditions, ex_conditions, self.max_node)
|
|
243
|
+
self.limit_dist = PlaceHolder(X=x_limit, E=e_limit, y=None)
|
|
244
|
+
self.noise_schedule = NoiseScheduleDiscrete(timesteps=self.timesteps).to(self.device)
|
|
245
|
+
|
|
246
|
+
def _initialize_model(
|
|
247
|
+
self,
|
|
248
|
+
model_class: Type[torch.nn.Module],
|
|
249
|
+
checkpoint: Optional[Dict] = None
|
|
250
|
+
) -> torch.nn.Module:
|
|
251
|
+
"""Initialize the model with parameters or a checkpoint."""
|
|
252
|
+
model_params = self._get_model_params(checkpoint)
|
|
253
|
+
self.model = model_class(**model_params)
|
|
254
|
+
self.model = self.model.to(self.device)
|
|
255
|
+
|
|
256
|
+
if checkpoint is not None:
|
|
257
|
+
self._setup_diffusion_params(checkpoint)
|
|
258
|
+
self.model.load_state_dict(checkpoint["model_state_dict"])
|
|
259
|
+
return self.model
|
|
260
|
+
|
|
261
|
+
def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
|
|
262
|
+
"""Setup optimization components including optimizer and learning rate scheduler.
|
|
263
|
+
"""
|
|
264
|
+
optimizer = torch.optim.Adam(
|
|
265
|
+
self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay
|
|
266
|
+
)
|
|
267
|
+
scheduler = None
|
|
268
|
+
if self.use_lr_scheduler:
|
|
269
|
+
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
270
|
+
optimizer,
|
|
271
|
+
mode="min",
|
|
272
|
+
factor=self.scheduler_factor,
|
|
273
|
+
patience=self.scheduler_patience,
|
|
274
|
+
min_lr=1e-6,
|
|
275
|
+
cooldown=0,
|
|
276
|
+
eps=1e-8,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return optimizer, scheduler
|
|
280
|
+
|
|
281
|
+
def fit(
|
|
282
|
+
self,
|
|
283
|
+
X_train: List[str],
|
|
284
|
+
y_train: Optional[Union[List, np.ndarray]] = None,
|
|
285
|
+
) -> "GraphDITMolecularGenerator":
|
|
286
|
+
num_task = len(self.task_type)
|
|
287
|
+
X_train, y_train = self._validate_inputs(X_train, y_train, num_task=num_task)
|
|
288
|
+
self._setup_diffusion_params(X_train)
|
|
289
|
+
self._initialize_model(self.model_class)
|
|
290
|
+
self.model.initialize_parameters()
|
|
291
|
+
|
|
292
|
+
optimizer, scheduler = self._setup_optimizers()
|
|
293
|
+
train_dataset = self._convert_to_pytorch_data(X_train, y_train)
|
|
294
|
+
train_loader = DataLoader(
|
|
295
|
+
train_dataset,
|
|
296
|
+
batch_size=self.batch_size,
|
|
297
|
+
shuffle=True,
|
|
298
|
+
num_workers=0
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
self.fitting_loss = []
|
|
302
|
+
self.fitting_epoch = 0
|
|
303
|
+
for epoch in range(self.epochs):
|
|
304
|
+
train_losses = self._train_epoch(train_loader, optimizer, epoch)
|
|
305
|
+
self.fitting_loss.append(np.mean(train_losses).item())
|
|
306
|
+
if scheduler:
|
|
307
|
+
scheduler.step(np.mean(train_losses).item())
|
|
308
|
+
|
|
309
|
+
self.fitting_epoch = epoch
|
|
310
|
+
self.is_fitted_ = True
|
|
311
|
+
return self
|
|
312
|
+
|
|
313
|
+
def _train_epoch(self, train_loader, optimizer, epoch):
|
|
314
|
+
self.model.train()
|
|
315
|
+
losses = []
|
|
316
|
+
iterator = (
|
|
317
|
+
tqdm(train_loader, desc="Training", leave=False)
|
|
318
|
+
if self.verbose
|
|
319
|
+
else train_loader
|
|
320
|
+
)
|
|
321
|
+
active_index = self.dataset_info["active_index"]
|
|
322
|
+
for step, batched_data in enumerate(iterator):
|
|
323
|
+
batched_data = batched_data.to(self.device)
|
|
324
|
+
optimizer.zero_grad()
|
|
325
|
+
|
|
326
|
+
data_x = F.one_hot(batched_data.x, num_classes=118).float()[:, active_index]
|
|
327
|
+
data_edge_attr = F.one_hot(batched_data.edge_attr, num_classes=5).float()
|
|
328
|
+
dense_data, node_mask = to_dense(data_x, batched_data.edge_index, data_edge_attr, batched_data.batch, self.max_node)
|
|
329
|
+
dense_data = dense_data.mask(node_mask)
|
|
330
|
+
X, E = dense_data.X, dense_data.E
|
|
331
|
+
noisy_data = self.apply_noise(X, E, batched_data.y, node_mask)
|
|
332
|
+
|
|
333
|
+
loss, loss_X, loss_E = self.model.compute_loss(noisy_data, true_X=X, true_E=E, lw_X=self.lw_X, lw_E=self.lw_E)
|
|
334
|
+
loss.backward()
|
|
335
|
+
if self.grad_clip_value is not None:
|
|
336
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
|
|
337
|
+
|
|
338
|
+
optimizer.step()
|
|
339
|
+
losses.append(loss.item())
|
|
340
|
+
|
|
341
|
+
if self.verbose:
|
|
342
|
+
iterator.set_postfix({"Epoch": epoch, "Loss": f"{loss.item():.4f}", "Loss_X": f"{loss_X.item():.4f}", "Loss_E": f"{loss_E.item():.4f}"})
|
|
343
|
+
|
|
344
|
+
return losses
|
|
345
|
+
|
|
346
|
+
def apply_noise(self, X, E, y, node_mask) -> Dict[str, Any]:
|
|
347
|
+
t_int = torch.randint(0, self.timesteps + 1, size=(X.size(0), 1), device=X.device).float() # (bs, 1)
|
|
348
|
+
s_int = t_int - 1
|
|
349
|
+
|
|
350
|
+
t_float = t_int / self.timesteps
|
|
351
|
+
s_float = s_int / self.timesteps
|
|
352
|
+
|
|
353
|
+
# beta_t and alpha_s_bar are used for denoising/loss computation
|
|
354
|
+
beta_t = self.noise_schedule(t_normalized=t_float) # (bs, 1)
|
|
355
|
+
alpha_s_bar = self.noise_schedule.get_alpha_bar(t_normalized=s_float) # (bs, 1)
|
|
356
|
+
alpha_t_bar = self.noise_schedule.get_alpha_bar(t_normalized=t_float) # (bs, 1)
|
|
357
|
+
|
|
358
|
+
Qtb = self.transition_model.get_Qt_bar(alpha_t_bar, self.device) # (bs, dx_in, dx_out), (bs, de_in, de_out)
|
|
359
|
+
|
|
360
|
+
bs, n, _ = X.shape
|
|
361
|
+
X_all = torch.cat([X, E.reshape(bs, n, -1)], dim=-1)
|
|
362
|
+
prob_all = X_all @ Qtb.X
|
|
363
|
+
probX = prob_all[:, :, :self.input_dim_X]
|
|
364
|
+
probE = prob_all[:, :, self.input_dim_X:].reshape(bs, n, n, -1)
|
|
365
|
+
|
|
366
|
+
# check whether X_all/prob_all/probX/probE contain nan
|
|
367
|
+
|
|
368
|
+
sampled_t = sample_discrete_features(probX=probX, probE=probE, node_mask=node_mask)
|
|
369
|
+
|
|
370
|
+
X_t = F.one_hot(sampled_t.X, num_classes=self.input_dim_X)
|
|
371
|
+
E_t = F.one_hot(sampled_t.E, num_classes=self.input_dim_E)
|
|
372
|
+
assert (X.shape == X_t.shape) and (E.shape == E_t.shape)
|
|
373
|
+
|
|
374
|
+
z_t = PlaceHolder(X=X_t, E=E_t, y=y).type_as(X_t).mask(node_mask)
|
|
375
|
+
|
|
376
|
+
noisy_data = {'t': t_float * self.timesteps, 'beta_t': beta_t, 'alpha_s_bar': alpha_s_bar,
|
|
377
|
+
'alpha_t_bar': alpha_t_bar, 'X_t': z_t.X, 'E_t': z_t.E, 'y_t': z_t.y, 'node_mask': node_mask}
|
|
378
|
+
|
|
379
|
+
return noisy_data
|
|
380
|
+
|
|
381
|
+
@torch.no_grad()
|
|
382
|
+
def generate(self, labels: Optional[Union[List[List], np.ndarray, torch.Tensor]] = None, num_nodes: Optional[Union[List[List], np.ndarray, torch.Tensor]] = None, batch_size: int = 32) -> List[str]:
|
|
383
|
+
"""Generate molecules with specified properties and optional node counts.
|
|
384
|
+
|
|
385
|
+
Parameters
|
|
386
|
+
----------
|
|
387
|
+
labels : Optional[Union[List[List], np.ndarray, torch.Tensor]], default=None
|
|
388
|
+
Target properties for the generated molecules. Can be provided as:
|
|
389
|
+
- A list of lists for multiple properties
|
|
390
|
+
- A numpy array of shape (batch_size, n_properties)
|
|
391
|
+
- A torch tensor of shape (batch_size, n_properties)
|
|
392
|
+
For single label (properties values), can also be provided as 1D array/tensor.
|
|
393
|
+
If None, generates unconditional samples.
|
|
394
|
+
|
|
395
|
+
num_nodes : Optional[Union[List[List], np.ndarray, torch.Tensor]], default=None
|
|
396
|
+
Number of nodes for each molecule in the batch. If None, samples from
|
|
397
|
+
the training distribution. Can be provided as:
|
|
398
|
+
- A list of lists
|
|
399
|
+
- A numpy array of shape (batch_size, 1)
|
|
400
|
+
- A torch tensor of shape (batch_size, 1)
|
|
401
|
+
|
|
402
|
+
batch_size : int, default=32
|
|
403
|
+
Number of molecules to generate. Only used if labels is None.
|
|
404
|
+
|
|
405
|
+
Returns
|
|
406
|
+
-------
|
|
407
|
+
List[str]
|
|
408
|
+
List of generated molecules in SMILES format.
|
|
409
|
+
"""
|
|
410
|
+
if not self.is_fitted_:
|
|
411
|
+
raise ValueError("Model must be fitted before generating molecules.")
|
|
412
|
+
if self.input_dim_X is None or self.input_dim_E is None or self.max_node is None:
|
|
413
|
+
raise ValueError(f"Model may not be fitted correctly as one of below attributes is not set: input_dim_X={self.input_dim_X}, input_dim_E={self.input_dim_E}, max_node={self.max_node}")
|
|
414
|
+
|
|
415
|
+
if len(self.task_type) > 0 and labels is None:
|
|
416
|
+
raise ValueError(f"labels must be provided if task_type is not empty: {self.task_type}")
|
|
417
|
+
|
|
418
|
+
if labels is not None and num_nodes is not None:
|
|
419
|
+
assert len(labels) == len(num_nodes), "labels and num_nodes must have the same batch size"
|
|
420
|
+
|
|
421
|
+
if labels is not None:
|
|
422
|
+
if num_nodes is not None:
|
|
423
|
+
assert len(labels) == len(num_nodes), "labels and num_nodes must have the same batch size"
|
|
424
|
+
batch_size = len(labels)
|
|
425
|
+
elif num_nodes is not None:
|
|
426
|
+
batch_size = len(num_nodes)
|
|
427
|
+
|
|
428
|
+
# Convert properties to 2D tensor if needed
|
|
429
|
+
if isinstance(labels, list):
|
|
430
|
+
labels = torch.tensor(labels)
|
|
431
|
+
elif isinstance(labels, np.ndarray):
|
|
432
|
+
labels = torch.from_numpy(labels)
|
|
433
|
+
if labels is not None and labels.dim() == 1:
|
|
434
|
+
labels = labels.unsqueeze(-1)
|
|
435
|
+
|
|
436
|
+
if num_nodes is None:
|
|
437
|
+
num_nodes_dist = self.dataset_info["num_nodes_dist"]
|
|
438
|
+
num_nodes = num_nodes_dist.sample_n(batch_size, self.device)
|
|
439
|
+
elif isinstance(num_nodes, list):
|
|
440
|
+
num_nodes = torch.tensor(num_nodes).to(self.device)
|
|
441
|
+
elif isinstance(num_nodes, np.ndarray):
|
|
442
|
+
num_nodes = torch.from_numpy(num_nodes).to(self.device)
|
|
443
|
+
if num_nodes.dim() == 1:
|
|
444
|
+
num_nodes = num_nodes.unsqueeze(-1)
|
|
445
|
+
|
|
446
|
+
assert num_nodes.size(0) == batch_size
|
|
447
|
+
arange = (
|
|
448
|
+
torch.arange(self.max_node).to(self.device)
|
|
449
|
+
.unsqueeze(0)
|
|
450
|
+
.expand(batch_size, -1)
|
|
451
|
+
)
|
|
452
|
+
node_mask = arange < num_nodes
|
|
453
|
+
|
|
454
|
+
if not hasattr(self, 'limit_dist') or self.limit_dist is None:
|
|
455
|
+
raise ValueError("Limit distribution not found. Please call setup_diffusion_params first.")
|
|
456
|
+
if not hasattr(self, 'dataset_info') or self.dataset_info is None:
|
|
457
|
+
raise ValueError("Dataset info not found. Please call setup_diffusion_params first.")
|
|
458
|
+
|
|
459
|
+
z_T = sample_discrete_feature_noise(
|
|
460
|
+
limit_dist=self.limit_dist, node_mask=node_mask
|
|
461
|
+
)
|
|
462
|
+
X, E = z_T.X, z_T.E
|
|
463
|
+
|
|
464
|
+
assert (E == torch.transpose(E, 1, 2)).all()
|
|
465
|
+
|
|
466
|
+
# Iteratively sample p(z_s | z_t) for t = 1, ..., T, with s = t - 1.
|
|
467
|
+
if labels is not None:
|
|
468
|
+
y = labels.to(self.device).float()
|
|
469
|
+
else:
|
|
470
|
+
y = None
|
|
471
|
+
|
|
472
|
+
self.model.eval()
|
|
473
|
+
for s_int in reversed(range(0, self.timesteps)):
|
|
474
|
+
s_array = s_int * torch.ones((batch_size, 1)).float().to(self.device)
|
|
475
|
+
t_array = s_array + 1
|
|
476
|
+
s_norm = s_array / self.timesteps
|
|
477
|
+
t_norm = t_array / self.timesteps
|
|
478
|
+
|
|
479
|
+
# Sample z_s
|
|
480
|
+
sampled_s = self.sample_p_zs_given_zt(s_norm, t_norm, X, E, y, node_mask)
|
|
481
|
+
X, E, y = sampled_s.X, sampled_s.E, sampled_s.y
|
|
482
|
+
|
|
483
|
+
# Sample
|
|
484
|
+
sampled_s = sampled_s.mask(node_mask, collapse=True)
|
|
485
|
+
X, E, y = sampled_s.X, sampled_s.E, sampled_s.y
|
|
486
|
+
|
|
487
|
+
molecule_list = []
|
|
488
|
+
for i in range(batch_size):
|
|
489
|
+
n = num_nodes[i][0].item()
|
|
490
|
+
atom_types = X[i, :n].cpu()
|
|
491
|
+
edge_types = E[i, :n, :n].cpu()
|
|
492
|
+
molecule_list.append([atom_types, edge_types])
|
|
493
|
+
|
|
494
|
+
smiles_list = graph_to_smiles(molecule_list, self.dataset_info["atom_decoder"])
|
|
495
|
+
return smiles_list
|
|
496
|
+
|
|
497
|
+
def sample_p_zs_given_zt(
|
|
498
|
+
self, s, t, X_t, E_t, properties, node_mask
|
|
499
|
+
):
|
|
500
|
+
"""Samples from zs ~ p(zs | zt). Only used during sampling.
|
|
501
|
+
if last_step, return the graph prediction as well"""
|
|
502
|
+
bs, n, _ = X_t.shape
|
|
503
|
+
beta_t = self.noise_schedule(t_normalized=t) # (bs, 1)
|
|
504
|
+
alpha_s_bar = self.noise_schedule.get_alpha_bar(t_normalized=s)
|
|
505
|
+
alpha_t_bar = self.noise_schedule.get_alpha_bar(t_normalized=t)
|
|
506
|
+
|
|
507
|
+
# Neural net predictions
|
|
508
|
+
noisy_data = {
|
|
509
|
+
"X_t": X_t,
|
|
510
|
+
"E_t": E_t,
|
|
511
|
+
"y_t": properties,
|
|
512
|
+
"t": t,
|
|
513
|
+
"node_mask": node_mask,
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
def get_prob(noisy_data, unconditioned=False):
|
|
517
|
+
pred = self.model(noisy_data, unconditioned=unconditioned)
|
|
518
|
+
|
|
519
|
+
# Normalize predictions
|
|
520
|
+
pred_X = F.softmax(pred.X, dim=-1) # bs, n, d0
|
|
521
|
+
pred_E = F.softmax(pred.E, dim=-1) # bs, n, n, d0
|
|
522
|
+
|
|
523
|
+
device = pred_X.device
|
|
524
|
+
# Retrieve transitions matrix
|
|
525
|
+
Qtb = self.transition_model.get_Qt_bar(alpha_t_bar, device)
|
|
526
|
+
Qsb = self.transition_model.get_Qt_bar(alpha_s_bar, device)
|
|
527
|
+
Qt = self.transition_model.get_Qt(beta_t, device)
|
|
528
|
+
|
|
529
|
+
Xt_all = torch.cat([X_t, E_t.reshape(bs, n, -1)], dim=-1)
|
|
530
|
+
predX_all = torch.cat([pred_X, pred_E.reshape(bs, n, -1)], dim=-1)
|
|
531
|
+
|
|
532
|
+
# raise ValueError('stop here')
|
|
533
|
+
unnormalized_probX_all = reverse_diffusion(
|
|
534
|
+
predX_0=predX_all, X_t=Xt_all, Qt=Qt.X, Qsb=Qsb.X, Qtb=Qtb.X
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
unnormalized_prob_X = unnormalized_probX_all[:, :, : self.input_dim_X]
|
|
538
|
+
unnormalized_prob_E = unnormalized_probX_all[
|
|
539
|
+
:, :, self.input_dim_X :
|
|
540
|
+
].reshape(bs, n * n, -1)
|
|
541
|
+
|
|
542
|
+
unnormalized_prob_X[torch.sum(unnormalized_prob_X, dim=-1) == 0] = 1e-5
|
|
543
|
+
unnormalized_prob_E[torch.sum(unnormalized_prob_E, dim=-1) == 0] = 1e-5
|
|
544
|
+
|
|
545
|
+
prob_X = unnormalized_prob_X / torch.sum(
|
|
546
|
+
unnormalized_prob_X, dim=-1, keepdim=True
|
|
547
|
+
) # bs, n, d_t-1
|
|
548
|
+
prob_E = unnormalized_prob_E / torch.sum(
|
|
549
|
+
unnormalized_prob_E, dim=-1, keepdim=True
|
|
550
|
+
) # bs, n, d_t-1
|
|
551
|
+
prob_E = prob_E.reshape(bs, n, n, pred_E.shape[-1])
|
|
552
|
+
|
|
553
|
+
return prob_X, prob_E
|
|
554
|
+
|
|
555
|
+
prob_X, prob_E = get_prob(noisy_data)
|
|
556
|
+
|
|
557
|
+
### Guidance
|
|
558
|
+
if self.guide_scale is not None and self.guide_scale != 1:
|
|
559
|
+
uncon_prob_X, uncon_prob_E = get_prob(noisy_data, unconditioned=True)
|
|
560
|
+
prob_X = (
|
|
561
|
+
uncon_prob_X
|
|
562
|
+
* (prob_X / uncon_prob_X.clamp_min(1e-5)) ** self.guide_scale
|
|
563
|
+
)
|
|
564
|
+
prob_E = (
|
|
565
|
+
uncon_prob_E
|
|
566
|
+
* (prob_E / uncon_prob_E.clamp_min(1e-5)) ** self.guide_scale
|
|
567
|
+
)
|
|
568
|
+
prob_X = prob_X / prob_X.sum(dim=-1, keepdim=True).clamp_min(1e-5)
|
|
569
|
+
prob_E = prob_E / prob_E.sum(dim=-1, keepdim=True).clamp_min(1e-5)
|
|
570
|
+
|
|
571
|
+
sampled_s = sample_discrete_features(prob_X, prob_E, node_mask=node_mask)
|
|
572
|
+
|
|
573
|
+
X_s = F.one_hot(sampled_s.X, num_classes=self.input_dim_X).to(self.device).float()
|
|
574
|
+
E_s = F.one_hot(sampled_s.E, num_classes=self.input_dim_E).to(self.device).float()
|
|
575
|
+
|
|
576
|
+
assert (E_s == torch.transpose(E_s, 1, 2)).all()
|
|
577
|
+
assert (X_t.shape == X_s.shape) and (E_t.shape == E_s.shape)
|
|
578
|
+
|
|
579
|
+
out_one_hot = PlaceHolder(X=X_s, E=E_s, y=properties)
|
|
580
|
+
|
|
581
|
+
return out_one_hot.mask(node_mask)
|