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,427 @@
1
+ import re
2
+ from tqdm import tqdm
3
+ from typing import Optional, Dict, Any, Tuple, List, Type
4
+ from dataclasses import dataclass, field
5
+
6
+ import numpy as np
7
+ import torch
8
+ import torch.nn.functional as F
9
+ from torch.utils.data import DataLoader
10
+ from torch.cuda.amp import GradScaler
11
+
12
+ from .gpt import GPT
13
+ from .dataset import SmilesDataset
14
+ from ...base import BaseMolecularGenerator
15
+
16
+ @dataclass
17
+ class MolGPTMolecularGenerator(BaseMolecularGenerator):
18
+ """
19
+ This generator implements the molecular GPT model for generating molecules.
20
+
21
+ The model uses a GPT-like architecture to learn the distribution of SMILES strings
22
+ and generate new molecules. It supports conditional generation based on properties
23
+ and/or molecular scaffolds.
24
+
25
+ References
26
+ ----------
27
+ - MolGPT: Molecular Generation Using a Transformer-Decoder Model. Journal of Chemical
28
+ Information and Modeling. https://pubs.acs.org/doi/10.1021/acs.jcim.1c00600
29
+ - Code: https://github.com/devalab/molgpt
30
+
31
+ Parameters
32
+ ----------
33
+ num_layer : int, default=8
34
+ Number of transformer layers in the model.
35
+ num_head : int, default=8
36
+ Number of attention heads in each transformer layer.
37
+ hidden_size : int, default=256
38
+ Dimension of the hidden representations.
39
+ max_len : int, default=128
40
+ Maximum length of SMILES strings.
41
+ num_task : int, default=0
42
+ Number of property prediction tasks for conditional generation. O for unconditional generation.
43
+ use_scaffold : bool, default=False
44
+ Whether to use scaffold conditioning.
45
+ use_lstm : bool, default=False
46
+ Whether to use LSTM for encoding scaffold.
47
+ lstm_layers : int, default=0
48
+ Number of LSTM layers if use_lstm is True.
49
+ batch_size : int, default=64
50
+ Batch size for training.
51
+ epochs : int, default=1000
52
+ Number of training epochs.
53
+ learning_rate : float, default=3e-4
54
+ Learning rate for optimizer.
55
+ adamw_betas : Tuple[float, float], default=(0.9, 0.95)
56
+ Beta parameters for AdamW optimizer.
57
+ weight_decay : float, default=0.1
58
+ Weight decay for optimizer.
59
+ grad_norm_clip : float, default=1.0
60
+ Gradient norm clipping value.
61
+ verbose : bool, default=False
62
+ Whether to display progress bars during training.
63
+ """
64
+
65
+ # Model parameters
66
+ num_layer: int = 8
67
+ num_head: int = 8
68
+ hidden_size: int = 256
69
+ max_len: int = 128
70
+
71
+ # Conditioning parameters
72
+ num_task: int = 0
73
+ use_scaffold: bool = False
74
+ use_lstm: bool = False
75
+ lstm_layers: int = 0
76
+
77
+ # Training parameters
78
+ batch_size: int = 64
79
+ epochs: int = 1000
80
+ learning_rate: float = 3e-4
81
+ adamw_betas: Tuple[float, float] = (0.9, 0.95)
82
+ weight_decay: float = 0.1
83
+ grad_norm_clip: float = 1.0
84
+
85
+ verbose: bool = False
86
+ # Attributes
87
+ model_name: str = "MolGPTMolecularGenerator"
88
+ fitting_loss: List[float] = field(default_factory=list, init=False)
89
+ fitting_epoch: int = field(default=0, init=False)
90
+ model_class: Type[GPT] = field(default=GPT, init=False)
91
+
92
+ def __post_init__(self):
93
+ """Initialize the model after dataclass initialization."""
94
+ super().__post_init__()
95
+ self.vocab_size = None
96
+ self.token_to_id = None
97
+ self.id_to_token = None
98
+ self.scaffold_maxlen = None
99
+ self.pattern = "(\[[^\]]+]|<|Br?|Cl?|N|O|S|P|F|I|b|c|n|o|s|p|\(|\)|\.|=|#|-|\+|\\\\|\/|:|~|@|\?|>|\*|\$|\%[0-9]{2}|[0-9])"
100
+ self.regex = re.compile(self.pattern)
101
+
102
+ @staticmethod
103
+ def _get_param_names() -> List[str]:
104
+ """Get parameter names for the estimator.
105
+
106
+ Returns
107
+ -------
108
+ List[str]
109
+ List of parameter names that can be used for model configuration.
110
+ """
111
+ return [
112
+ # Model Hyperparameters
113
+ "num_layer", "num_head", "hidden_size",
114
+ # Conditioning parameters
115
+ "num_task", "use_scaffold", "use_lstm", "lstm_layers",
116
+ # post-initialization parameters
117
+ "vocab_size", "token_to_id", "id_to_token", "scaffold_maxlen", "max_len",
118
+ # Training Parameters
119
+ "batch_size", "epochs", "learning_rate", "weight_decay", "grad_norm_clip",
120
+ # Other Parameters
121
+ "verbose", "model_name", "fitting_epoch", "fitting_loss", "device"
122
+ ]
123
+
124
+ def _initialize_model(
125
+ self,
126
+ model_class: Type[torch.nn.Module],
127
+ checkpoint: Optional[Dict] = None
128
+ ) -> torch.nn.Module:
129
+ """Initialize the model with parameters or a checkpoint.
130
+
131
+ Parameters
132
+ ----------
133
+ model_class : Type[torch.nn.Module]
134
+ PyTorch module class to instantiate
135
+ checkpoint : Optional[Dict], default=None
136
+ Optional dictionary containing model checkpoint data
137
+
138
+ Returns
139
+ -------
140
+ torch.nn.Module
141
+ Initialized PyTorch model
142
+ """
143
+ model_params = self._get_model_params(checkpoint)
144
+ self.model = model_class(**model_params)
145
+ self.model = self.model.to(self.device)
146
+
147
+ if checkpoint is not None:
148
+ self.model.load_state_dict(checkpoint["model_state_dict"])
149
+ # get other params in checkpoint["hyperparameters"] but NOT in model_params
150
+ other_params = {k: checkpoint["hyperparameters"][k] for k in checkpoint["hyperparameters"] if k not in model_params}
151
+ # set other params in self
152
+ for k, v in other_params.items():
153
+ setattr(self, k, v)
154
+ return self.model
155
+
156
+ def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
157
+ params = [
158
+ "vocab_size", "max_len", "num_task", "num_layer", "num_head", "hidden_size", "use_scaffold", "scaffold_maxlen", "use_lstm", "lstm_layers",
159
+ ]
160
+ if checkpoint is not None:
161
+ if "hyperparameters" not in checkpoint:
162
+ raise ValueError("Checkpoint missing 'hyperparameters' key")
163
+ return {k: checkpoint["hyperparameters"][k] for k in params}
164
+
165
+ return {k: getattr(self, k) for k in params}
166
+
167
+ def _setup_optimizers(self):
168
+ train_config = {
169
+ "learning_rate": self.learning_rate,
170
+ "weight_decay": self.weight_decay,
171
+ "betas": self.adamw_betas
172
+ }
173
+ return self.model.configure_optimizers(train_config)
174
+
175
+ def fit(self, X_train, y_train=None, X_scaffold=None):
176
+ """
177
+ Train the MolGPT model on SMILES strings.
178
+
179
+ Parameters
180
+ ----------
181
+ X_train : List[str]
182
+ List of SMILES strings for training
183
+ y_train : Optional[List[float]]
184
+ Optional list of property values for conditional generation
185
+ X_scaffold : Optional[List[str]]
186
+ Optional list of scaffold SMILES strings for conditional generation
187
+
188
+ Returns
189
+ -------
190
+ self : MolGPTGenerator
191
+ The fitted model
192
+ """
193
+ X_train, y_train = self._validate_inputs(X_train, y_train, num_task=self.num_task, return_rdkit_mol=False)
194
+
195
+ # Calculate max length for padding
196
+ lens = [len(self.regex.findall(i.strip())) for i in X_train]
197
+ max_len = max(lens)
198
+
199
+ if X_scaffold is not None:
200
+ assert len(X_scaffold) == len(X_train), "X_scaffold and X_train must have the same length"
201
+ assert self.use_scaffold, "use_scaffold must be True"
202
+ X_scaffold, _ = self._validate_inputs(X_scaffold, num_task=self.num_task, return_rdkit_mol=False)
203
+ scaffold_maxlen = max([len(self.regex.findall(i.strip())) for i in X_scaffold])
204
+ else:
205
+ scaffold_maxlen = 0
206
+
207
+ self.scaffold_maxlen = scaffold_maxlen
208
+ self.max_len = max_len
209
+
210
+ # Create dataset
211
+ train_dataset = SmilesDataset(
212
+ X_train,
213
+ self.regex,
214
+ max_len,
215
+ properties=y_train,
216
+ scaffolds=X_scaffold,
217
+ scaffold_maxlen=scaffold_maxlen,
218
+ )
219
+
220
+ # Save vocabulary
221
+ self.vocab_size = train_dataset.vocab_size
222
+ self.token_to_id = train_dataset.stoi
223
+ self.id_to_token = train_dataset.itos
224
+
225
+ # Initialize model
226
+ self._initialize_model(self.model_class)
227
+ self.model.initialize_parameters()
228
+ optimizer = self._setup_optimizers()
229
+
230
+ # Create data loader
231
+ train_loader = DataLoader(
232
+ train_dataset,
233
+ batch_size=self.batch_size,
234
+ shuffle=True,
235
+ num_workers=0
236
+ )
237
+
238
+ # Training loop
239
+ self.fitting_loss = []
240
+ self.fitting_epoch = 0
241
+
242
+ scaler = GradScaler()
243
+ for epoch in range(self.epochs):
244
+ train_losses = self._train_epoch(train_loader, optimizer, epoch, scaler)
245
+ self.fitting_loss.append(np.mean(train_losses))
246
+
247
+ self.fitting_epoch = epoch
248
+ self.is_fitted_ = True
249
+ return self
250
+
251
+ def _train_epoch(self, train_loader, optimizer, epoch, scaler):
252
+ self.model.train()
253
+ losses = []
254
+
255
+ iterator = (
256
+ tqdm(train_loader, desc="Training", leave=False)
257
+ if self.verbose
258
+ else train_loader
259
+ )
260
+
261
+ for step, (x, y, prop, scaffold) in enumerate(iterator):
262
+ x = x.to(self.device)
263
+ y = y.to(self.device)
264
+ prop = prop.to(self.device) if prop.numel() > 0 else None
265
+ scaffold = scaffold.to(self.device) if scaffold.numel() > 0 else None
266
+
267
+ optimizer.zero_grad()
268
+ loss = self.model.compute_loss(x, targets=y, prop=prop, scaffold=scaffold)
269
+
270
+ scaler.scale(loss).backward()
271
+ scaler.unscale_(optimizer)
272
+ torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_norm_clip)
273
+ scaler.step(optimizer)
274
+ scaler.update()
275
+ losses.append(loss.item())
276
+
277
+ if self.verbose:
278
+ iterator.set_postfix({"Epoch": epoch, "Loss": f"{loss.item():.4f}"})
279
+
280
+ return losses
281
+
282
+ @torch.no_grad()
283
+ def sample(self, x, steps, temperature=1.0, top_k=None, prop=None, scaffold=None):
284
+ """
285
+ Sample from the model given a context.
286
+
287
+ Parameters
288
+ ----------
289
+ x : torch.Tensor
290
+ Context tensor of shape (batch_size, seq_len)
291
+ steps : int
292
+ Number of steps to sample
293
+ temperature : float
294
+ Sampling temperature
295
+ top_k : int
296
+ Top-k sampling parameter
297
+ prop : torch.Tensor
298
+ Property conditioning tensor
299
+ scaffold : torch.Tensor
300
+ Scaffold conditioning tensor
301
+
302
+ Returns
303
+ -------
304
+ torch.Tensor
305
+ Generated sequences
306
+ """
307
+ model = self.model
308
+ model.eval()
309
+
310
+ for k in range(steps):
311
+ # Get block size from model
312
+ max_len = model.get_max_len()
313
+
314
+ # Crop context if needed
315
+ x_cond = x if x.size(1) <= max_len else x[:, -max_len:]
316
+
317
+ # Forward pass
318
+ logits, _ = model(x_cond, prop=prop, scaffold=scaffold)
319
+
320
+ # Get logits for the next token and apply temperature
321
+ logits = logits[:, -1, :] / temperature
322
+
323
+ # Apply top-k sampling if specified
324
+ if top_k is not None:
325
+ v, _ = torch.topk(logits, top_k)
326
+ logits[logits < v[:, [-1]]] = -float('Inf')
327
+
328
+ # Apply softmax to get probabilities
329
+ probs = F.softmax(logits, dim=-1)
330
+
331
+ # Sample from the distribution
332
+ next_token = torch.multinomial(probs, num_samples=1)
333
+
334
+ # Append to the sequence
335
+ x = torch.cat((x, next_token), dim=1)
336
+
337
+ return x
338
+
339
+ def generate(self, n_samples=10, properties=None, scaffolds=None, max_len=None, temperature=1.0, top_k=10, starting_token='C'):
340
+ """
341
+ Generate molecules using the trained model.
342
+
343
+ Parameters
344
+ ----------
345
+ n_samples : int, default=10
346
+ Number of molecules to generate
347
+ properties : Optional[List[List[float]]]
348
+ Property values for conditional generation
349
+ scaffolds : Optional[List[str]]
350
+ Scaffold SMILES for conditional generation
351
+ max_len : Optional[int]
352
+ Maximum length of generated SMILES
353
+ temperature : float, default=1.0
354
+ Sampling temperature
355
+ top_k : int, default=10
356
+ Top-k sampling parameter
357
+ starting_token : Optional[str]
358
+ Starting token for generation (default is 'C')
359
+
360
+ Returns
361
+ -------
362
+ List[str]
363
+ List of generated SMILES strings
364
+ """
365
+ if not self.is_fitted_:
366
+ raise ValueError("Model must be fitted before generating molecules")
367
+
368
+ if max_len is None:
369
+ max_len = self.max_len
370
+
371
+ # Prepare property conditioning if provided
372
+ if properties is not None:
373
+ if len(properties) != n_samples:
374
+ raise ValueError(f"Number of property values ({len(properties)}) must match n_samples ({n_samples})")
375
+ prop_tensor = torch.tensor(properties, dtype=torch.float).to(self.device)
376
+ else:
377
+ prop_tensor = None
378
+
379
+ # Prepare scaffold conditioning if provided
380
+ if scaffolds is not None:
381
+ if len(scaffolds) != n_samples:
382
+ raise ValueError(f"Number of scaffolds ({len(scaffolds)}) must match n_samples ({n_samples})")
383
+
384
+ # Tokenize scaffolds
385
+ regex = re.compile(self.pattern)
386
+
387
+ scaffold_tokens = []
388
+ for scaffold in scaffolds:
389
+ tokens = regex.findall(scaffold.strip())
390
+ # Pad with '<' if needed
391
+ tokens += ['<'] * (self.scaffold_maxlen - len(tokens))
392
+ # Convert to indices
393
+ scaffold_tokens.append([self.token_to_id.get(t, 0) for t in tokens])
394
+
395
+ scaffold_tensor = torch.tensor(scaffold_tokens, dtype=torch.long).to(self.device)
396
+ else:
397
+ scaffold_tensor = None
398
+
399
+ if starting_token in self.token_to_id:
400
+ start_token = self.token_to_id[starting_token]
401
+ else:
402
+ warnings.warn(f"Starting token {starting_token} not found in vocabulary, using first token instead")
403
+ start_token = 0
404
+
405
+ context = torch.tensor([[start_token]] * n_samples, dtype=torch.long).to(self.device)
406
+
407
+ # Sample from the model
408
+ generated = self.sample(
409
+ context,
410
+ steps=max_len-1, # -1 because we already have one token
411
+ temperature=temperature,
412
+ top_k=top_k,
413
+ prop=prop_tensor,
414
+ scaffold=scaffold_tensor
415
+ )
416
+
417
+ # Convert to SMILES strings
418
+ smiles_list = []
419
+ for i in range(n_samples):
420
+ # Convert indices to tokens
421
+ tokens = [self.id_to_token[idx.item()] for idx in generated[i]]
422
+ # Join tokens and remove padding
423
+ smiles = ''.join(tokens).replace('<', '')
424
+ smiles_list.append(smiles)
425
+
426
+ return smiles_list
427
+
@@ -0,0 +1,16 @@
1
+ from .gnn import GNN_node, GNN_node_Virtualnode, GCNConv, GINConv
2
+ from .mlp import MLP
3
+ from .attention import AttentionWithNodeMask
4
+ from .embedder import TimestepEmbedder, CategoricalEmbedder, ClusterContinuousEmbedder
5
+
6
+ __all__ = [
7
+ "MLP",
8
+ "GCNConv",
9
+ "GINConv",
10
+ "GNN_node",
11
+ "GNN_node_Virtualnode",
12
+ "AttentionWithNodeMask",
13
+ "TimestepEmbedder",
14
+ "CategoricalEmbedder",
15
+ "ClusterContinuousEmbedder",
16
+ ]
@@ -0,0 +1,63 @@
1
+ from torch.jit import Final
2
+ import torch.nn.functional as F
3
+ from itertools import repeat
4
+ import collections.abc
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+
9
+ class AttentionWithNodeMask(nn.Module):
10
+ fast_attn: Final[bool]
11
+
12
+ def __init__(
13
+ self,
14
+ dim,
15
+ num_head=8,
16
+ qkv_bias=False,
17
+ qk_norm=False,
18
+ attn_drop=0.,
19
+ proj_drop=0.,
20
+ norm_layer=nn.LayerNorm,
21
+ ):
22
+ super().__init__()
23
+ assert dim % num_head == 0, 'dim should be divisible by num_head'
24
+ self.num_head = num_head
25
+ self.head_dim = dim // num_head
26
+
27
+ self.scale = self.head_dim ** -0.5
28
+ self.fast_attn = hasattr(torch.nn.functional, 'scaled_dot_product_attention') # FIXME
29
+ assert self.fast_attn, "scaled_dot_product_attention Not implemented"
30
+
31
+ self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
32
+
33
+ self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
34
+ self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
35
+ self.attn_drop = nn.Dropout(attn_drop)
36
+
37
+ self.proj = nn.Linear(dim, dim)
38
+ self.proj_drop = nn.Dropout(proj_drop)
39
+
40
+ def forward(self, x, node_mask):
41
+ B, N, D = x.shape
42
+
43
+ # B, head, N, head_dim
44
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_head, self.head_dim).permute(2, 0, 3, 1, 4)
45
+ q, k, v = qkv.unbind(0) # B, head, N, head_dim
46
+ q, k = self.q_norm(q), self.k_norm(k)
47
+
48
+ attn_mask = (node_mask[:, None, :, None] & node_mask[:, None, None, :]).expand(-1, self.num_head, N, N)
49
+ attn_mask = attn_mask.clone()
50
+ attn_mask[attn_mask.sum(-1) == 0] = True
51
+
52
+ x = F.scaled_dot_product_attention(
53
+ q, k, v,
54
+ dropout_p=self.attn_drop.p,
55
+ attn_mask=attn_mask,
56
+ )
57
+
58
+ x = x.transpose(1, 2).reshape(B, N, -1)
59
+
60
+ x = self.proj(x)
61
+ x = self.proj_drop(x)
62
+
63
+ return x
@@ -0,0 +1,183 @@
1
+ import torch
2
+ import torch.nn as nn
3
+ import math
4
+
5
+ class TimestepEmbedder(nn.Module):
6
+ """
7
+ Embeds scalar timesteps into vector representations using a sinusoidal embedding
8
+ followed by a multilayer perceptron (MLP).
9
+
10
+ Parameters
11
+ ----------
12
+ hidden_size : int
13
+ Output dimension of the MLP embedding.
14
+ frequency_embedding_size : int, optional
15
+ Size of the input frequency embedding, by default 256.
16
+ """
17
+
18
+ def __init__(self, hidden_size, frequency_embedding_size=256):
19
+ super().__init__()
20
+ self.mlp = nn.Sequential(
21
+ nn.Linear(frequency_embedding_size, hidden_size, bias=True),
22
+ nn.SiLU(),
23
+ nn.Linear(hidden_size, hidden_size, bias=True),
24
+ )
25
+ self.frequency_embedding_size = frequency_embedding_size
26
+
27
+ @staticmethod
28
+ def timestep_embedding(t, dim, max_period=10000):
29
+ """
30
+ Create sinusoidal timestep embeddings.
31
+
32
+ :param t: a 1-D Tensor of N indices, one per batch element.
33
+ These may be fractional.
34
+ :param dim: the dimension of the output.
35
+ :param max_period: controls the minimum frequency of the embeddings.
36
+ :return: an (N, D) Tensor of positional embeddings.
37
+ """
38
+ # https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py
39
+ half = dim // 2
40
+ freqs = torch.exp(
41
+ -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
42
+ ).to(device=t.device)
43
+ args = t[:, None].float() * freqs[None]
44
+ embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
45
+ if dim % 2:
46
+ embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
47
+ return embedding
48
+
49
+ def forward(self, t):
50
+ """
51
+ Forward pass for timestep embedding.
52
+
53
+ Parameters
54
+ ----------
55
+ t : torch.Tensor
56
+ 1D tensor of scalar timesteps.
57
+
58
+ Returns
59
+ -------
60
+ torch.Tensor
61
+ The final embedded representation of shape (N, hidden_size).
62
+ """
63
+ t = t.view(-1)
64
+ t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
65
+ t_emb = self.mlp(t_freq)
66
+ return t_emb
67
+
68
+ class CategoricalEmbedder(nn.Module):
69
+ """
70
+ Embeds categorical conditions (e.g., data source labels) into vector representations.
71
+ Supports label dropout for classifier-free guidance.
72
+
73
+ Parameters
74
+ ----------
75
+ num_classes : int
76
+ Number of distinct label categories.
77
+ hidden_size : int
78
+ Size of the embedding vectors.
79
+ dropout_prob : float
80
+ Probability of label dropout.
81
+ """
82
+ def __init__(self, num_classes, hidden_size, dropout_prob):
83
+ super().__init__()
84
+ use_cfg_embedding = dropout_prob > 0
85
+ self.embedding_table = nn.Embedding(num_classes + use_cfg_embedding, hidden_size)
86
+ self.num_classes = num_classes
87
+ self.dropout_prob = dropout_prob
88
+
89
+ def forward(self, labels, train, force_drop_ids=None):
90
+ """
91
+ Forward pass for categorical embedding with optional label dropout.
92
+
93
+ Parameters
94
+ ----------
95
+ labels : torch.Tensor
96
+ Tensor of categorical labels.
97
+ train : bool
98
+ Whether the model is in training mode.
99
+ force_drop_ids : torch.Tensor or None, optional
100
+ Explicit mask for which labels to drop.
101
+
102
+ Returns
103
+ -------
104
+ torch.Tensor
105
+ Embedded label representations, with optional noise added during training.
106
+ """
107
+ labels = labels.long().view(-1)
108
+
109
+ use_dropout = self.dropout_prob > 0
110
+ drop_ids = force_drop_ids == 1
111
+
112
+ if (train and use_dropout):
113
+ drop_ids_rand = torch.rand(labels.shape[0], device=labels.device) < self.dropout_prob
114
+ if force_drop_ids is not None:
115
+ drop_ids = torch.logical_or(drop_ids, drop_ids_rand)
116
+ else:
117
+ drop_ids = drop_ids_rand
118
+
119
+ if use_dropout:
120
+ labels = torch.where(drop_ids, self.num_classes, labels)
121
+ embeddings = self.embedding_table(labels)
122
+ if train:
123
+ noise = torch.randn_like(embeddings)
124
+ embeddings = embeddings + noise
125
+ return embeddings
126
+
127
+ class ClusterContinuousEmbedder(nn.Module):
128
+ """
129
+ Embeds continuous input features into vector representations using a multilayer perceptron (MLP).
130
+ Supports optional embedding dropout for classifier-free guidance.
131
+
132
+ Parameters
133
+ ----------
134
+ input_size : int
135
+ The size of the input features.
136
+ hidden_size : int
137
+ The size of the output embedding vectors.
138
+ dropout_prob : float
139
+ Probability of embedding dropout, used for classifier-free guidance.
140
+
141
+ """
142
+ def __init__(self, input_size, hidden_size, dropout_prob):
143
+ super().__init__()
144
+ use_cfg_embedding = dropout_prob > 0
145
+
146
+ if use_cfg_embedding:
147
+ self.embedding_drop = nn.Embedding(1, hidden_size)
148
+
149
+ self.mlp = nn.Sequential(
150
+ nn.Linear(input_size, hidden_size, bias=True),
151
+ nn.Softmax(dim=1),
152
+ nn.Linear(hidden_size, hidden_size, bias=False)
153
+ )
154
+ self.hidden_size = hidden_size
155
+ self.dropout_prob = dropout_prob
156
+
157
+ def forward(self, labels, train, force_drop_ids=None):
158
+ use_dropout = self.dropout_prob > 0
159
+ if force_drop_ids is not None:
160
+ drop_ids = force_drop_ids == 1
161
+ else:
162
+ drop_ids = None
163
+ if not use_dropout:
164
+ drop_ids = None
165
+
166
+ if (train and use_dropout):
167
+ drop_ids_rand = torch.rand(labels.shape[0], device=labels.device) < self.dropout_prob
168
+ if force_drop_ids is not None:
169
+ drop_ids = torch.logical_or(drop_ids, drop_ids_rand)
170
+ else:
171
+ drop_ids = drop_ids_rand
172
+
173
+ if drop_ids is not None:
174
+ embeddings = torch.zeros((labels.shape[0], self.hidden_size), device=labels.device)
175
+ embeddings[~drop_ids] = self.mlp(labels[~drop_ids])
176
+ embeddings[drop_ids] += self.embedding_drop.weight[0]
177
+ else:
178
+ embeddings = self.mlp(labels)
179
+
180
+ if train:
181
+ noise = torch.randn_like(embeddings)
182
+ embeddings = embeddings + noise
183
+ return embeddings