torch-molecule 0.1.0.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. torch_molecule/__init__.py +70 -0
  2. torch_molecule/base/__init__.py +8 -0
  3. torch_molecule/base/base.py +396 -0
  4. torch_molecule/base/encoder.py +44 -0
  5. torch_molecule/base/generator.py +24 -0
  6. torch_molecule/base/old_encoder.py +322 -0
  7. torch_molecule/base/old_predictor.py +904 -0
  8. torch_molecule/base/old_predictor2.py +436 -0
  9. torch_molecule/base/predictor.py +112 -0
  10. torch_molecule/encoder/attrmask/__init__.py +3 -0
  11. torch_molecule/encoder/attrmask/model.py +113 -0
  12. torch_molecule/encoder/attrmask/modeling_attrmask.py +304 -0
  13. torch_molecule/encoder/constant.py +13 -0
  14. torch_molecule/encoder/contextpred/__init__.py +3 -0
  15. torch_molecule/encoder/contextpred/model.py +207 -0
  16. torch_molecule/encoder/contextpred/modeling_contextpred.py +299 -0
  17. torch_molecule/encoder/contextpred/utils.py +206 -0
  18. torch_molecule/encoder/edgepred/__init__.py +3 -0
  19. torch_molecule/encoder/edgepred/model.py +104 -0
  20. torch_molecule/encoder/edgepred/modeling_edgepred.py +285 -0
  21. torch_molecule/encoder/graphmae/__init__.py +3 -0
  22. torch_molecule/encoder/graphmae/dataloader.py +206 -0
  23. torch_molecule/encoder/graphmae/model.py +137 -0
  24. torch_molecule/encoder/graphmae/modeling_graphmae.py +323 -0
  25. torch_molecule/encoder/infograph/__init__.py +3 -0
  26. torch_molecule/encoder/infograph/loss_function.py +128 -0
  27. torch_molecule/encoder/infograph/model.py +92 -0
  28. torch_molecule/encoder/infograph/modeling_infograph.py +301 -0
  29. torch_molecule/encoder/infograph/module.py +31 -0
  30. torch_molecule/encoder/moama/__init__.py +3 -0
  31. torch_molecule/encoder/moama/model.py +131 -0
  32. torch_molecule/encoder/moama/modeling_moama.py +307 -0
  33. torch_molecule/encoder/moama/utils.py +143 -0
  34. torch_molecule/encoder/pretrained/__init__.py +3 -0
  35. torch_molecule/encoder/pretrained/modeling_pretrained.py +274 -0
  36. torch_molecule/encoder/supervised/__init__.py +3 -0
  37. torch_molecule/encoder/supervised/model.py +97 -0
  38. torch_molecule/encoder/supervised/modeling_supervised.py +354 -0
  39. torch_molecule/generator/digress/__init__.py +3 -0
  40. torch_molecule/generator/digress/diffusion.py +194 -0
  41. torch_molecule/generator/digress/modeling_digress.py +503 -0
  42. torch_molecule/generator/digress/transformer.py +360 -0
  43. torch_molecule/generator/digress/utils.py +196 -0
  44. torch_molecule/generator/gdss/__init__.py +3 -0
  45. torch_molecule/generator/gdss/layers.py +257 -0
  46. torch_molecule/generator/gdss/model.py +204 -0
  47. torch_molecule/generator/gdss/modeling_gdss.py +474 -0
  48. torch_molecule/generator/gdss/sde.py +334 -0
  49. torch_molecule/generator/gdss/solver.py +227 -0
  50. torch_molecule/generator/gdss/toremove/ScoreNetworkA.py +167 -0
  51. torch_molecule/generator/gdss/toremove/ScoreNetworkX.py +85 -0
  52. torch_molecule/generator/gdss/utils.py +240 -0
  53. torch_molecule/generator/graph_dit/__init__.py +3 -0
  54. torch_molecule/generator/graph_dit/diffusion.py +188 -0
  55. torch_molecule/generator/graph_dit/modeling_graph_dit.py +581 -0
  56. torch_molecule/generator/graph_dit/transformer.py +207 -0
  57. torch_molecule/generator/graph_dit/utils.py +220 -0
  58. torch_molecule/generator/graph_ga/__init__.py +3 -0
  59. torch_molecule/generator/graph_ga/crossover.py +182 -0
  60. torch_molecule/generator/graph_ga/modeling_graph_ga.py +310 -0
  61. torch_molecule/generator/graph_ga/mutate.py +136 -0
  62. torch_molecule/generator/graph_ga/oracle.py +95 -0
  63. torch_molecule/generator/jtvae/__init__.py +3 -0
  64. torch_molecule/generator/jtvae/jtnn/chemutils.py +450 -0
  65. torch_molecule/generator/jtvae/jtnn/datautils.py +127 -0
  66. torch_molecule/generator/jtvae/jtnn/jtmpn.py +140 -0
  67. torch_molecule/generator/jtvae/jtnn/jtnn_dec.py +350 -0
  68. torch_molecule/generator/jtvae/jtnn/jtnn_enc.py +131 -0
  69. torch_molecule/generator/jtvae/jtnn/mol_tree.py +111 -0
  70. torch_molecule/generator/jtvae/jtnn/mpn.py +129 -0
  71. torch_molecule/generator/jtvae/jtnn/nnutils.py +66 -0
  72. torch_molecule/generator/jtvae/jtnn/vocab.py +31 -0
  73. torch_molecule/generator/jtvae/jtnn_vae.py +246 -0
  74. torch_molecule/generator/jtvae/modeling_jtvae.py +247 -0
  75. torch_molecule/generator/lstm/__init__.py +3 -0
  76. torch_molecule/generator/lstm/action_sampler.py +91 -0
  77. torch_molecule/generator/lstm/lstm.py +59 -0
  78. torch_molecule/generator/lstm/modeling_lstm.py +263 -0
  79. torch_molecule/generator/lstm/smiles_char_dict.py +123 -0
  80. torch_molecule/generator/lstm/utils.py +62 -0
  81. torch_molecule/generator/molgpt/__init__.py +3 -0
  82. torch_molecule/generator/molgpt/dataset.py +87 -0
  83. torch_molecule/generator/molgpt/gpt.py +251 -0
  84. torch_molecule/generator/molgpt/modeling_molgpt.py +427 -0
  85. torch_molecule/generator/pretrained/modeling_pretrained.py +1 -0
  86. torch_molecule/nn/__init__.py +16 -0
  87. torch_molecule/nn/attention.py +63 -0
  88. torch_molecule/nn/embedder.py +183 -0
  89. torch_molecule/nn/gnn.py +631 -0
  90. torch_molecule/nn/mlp.py +34 -0
  91. torch_molecule/predictor/bfgnn/__init__.py +3 -0
  92. torch_molecule/predictor/bfgnn/model.py +116 -0
  93. torch_molecule/predictor/bfgnn/modeling_bfgnn.py +81 -0
  94. torch_molecule/predictor/dir/__init__.py +3 -0
  95. torch_molecule/predictor/dir/model.py +235 -0
  96. torch_molecule/predictor/dir/modeling_dir.py +161 -0
  97. torch_molecule/predictor/dir/utils.py +40 -0
  98. torch_molecule/predictor/gnn/__init__.py +3 -0
  99. torch_molecule/predictor/gnn/model.py +114 -0
  100. torch_molecule/predictor/gnn/modeling_gnn.py +706 -0
  101. torch_molecule/predictor/gnn/old_modeling_gnn.py +707 -0
  102. torch_molecule/predictor/grea/__init__.py +3 -0
  103. torch_molecule/predictor/grea/model.py +190 -0
  104. torch_molecule/predictor/grea/modeling_grea.py +107 -0
  105. torch_molecule/predictor/grea/old1_features.py +229 -0
  106. torch_molecule/predictor/grea/old1_modeling_grea.py +493 -0
  107. torch_molecule/predictor/grea/utils.py +97 -0
  108. torch_molecule/predictor/grin/__init__.py +3 -0
  109. torch_molecule/predictor/grin/model.py +116 -0
  110. torch_molecule/predictor/grin/modeling_grin.py +91 -0
  111. torch_molecule/predictor/irm/__init__.py +3 -0
  112. torch_molecule/predictor/irm/model.py +127 -0
  113. torch_molecule/predictor/irm/modeling_irm.py +179 -0
  114. torch_molecule/predictor/lstm/__init__.py +3 -0
  115. torch_molecule/predictor/lstm/model.py +145 -0
  116. torch_molecule/predictor/lstm/modeling_lstm.py +627 -0
  117. torch_molecule/predictor/lstm/token_from_smiles.py +85 -0
  118. torch_molecule/predictor/old_modeling_graph.py +650 -0
  119. torch_molecule/predictor/rpgnn/__init__.py +3 -0
  120. torch_molecule/predictor/rpgnn/model.py +185 -0
  121. torch_molecule/predictor/rpgnn/modeling_rpgnn.py +76 -0
  122. torch_molecule/predictor/sgir/__init__.py +3 -0
  123. torch_molecule/predictor/sgir/modeling_sgir.py +260 -0
  124. torch_molecule/predictor/sgir/strategy.py +266 -0
  125. torch_molecule/predictor/smiles_transformer/__init__.py +3 -0
  126. torch_molecule/predictor/smiles_transformer/model.py +424 -0
  127. torch_molecule/predictor/smiles_transformer/modeling_transformer.py +252 -0
  128. torch_molecule/predictor/ssr/__init__.py +3 -0
  129. torch_molecule/predictor/ssr/model.py +283 -0
  130. torch_molecule/predictor/ssr/modeling_ssr.py +267 -0
  131. torch_molecule/predictor/ssr/utils.py +185 -0
  132. torch_molecule/utils/__init__.py +31 -0
  133. torch_molecule/utils/checker.py +158 -0
  134. torch_molecule/utils/checkpoint.py +294 -0
  135. torch_molecule/utils/format.py +140 -0
  136. torch_molecule/utils/generic/metrics.py +530 -0
  137. torch_molecule/utils/generic/pseudo_tasks.py +11 -0
  138. torch_molecule/utils/generic/weights.py +30 -0
  139. torch_molecule/utils/graph/features.py +178 -0
  140. torch_molecule/utils/graph/graph_from_smiles.py +134 -0
  141. torch_molecule/utils/graph/graph_to_smiles.py +312 -0
  142. torch_molecule/utils/hf.py +232 -0
  143. torch_molecule/utils/search.py +64 -0
  144. torch_molecule-0.1.0.dev2.dist-info/METADATA +243 -0
  145. torch_molecule-0.1.0.dev2.dist-info/RECORD +148 -0
  146. torch_molecule-0.1.0.dev2.dist-info/WHEEL +5 -0
  147. torch_molecule-0.1.0.dev2.dist-info/licenses/LICENSE +21 -0
  148. torch_molecule-0.1.0.dev2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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)