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