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,299 @@
1
+ import os
2
+ import numpy as np
3
+ from tqdm import tqdm
4
+ from typing import Optional, Union, Dict, Any, Tuple, List, Callable, Literal, Type
5
+ import warnings
6
+ from dataclasses import dataclass, field
7
+
8
+ import torch
9
+ from torch_geometric.loader import DataLoader
10
+ from torch_geometric.data import Data
11
+
12
+ from .model import GNN
13
+ from ..constant import GNN_ENCODER_MODELS, GNN_ENCODER_READOUTS, GNN_ENCODER_PARAMS
14
+ from ...base import BaseMolecularEncoder
15
+ from ...utils import graph_from_smiles
16
+
17
+ ALLOWABLE_ENCODER_MODELS = GNN_ENCODER_MODELS
18
+ ALLOWABLE_ENCODER_READOUTS = GNN_ENCODER_READOUTS
19
+
20
+ @dataclass
21
+ class ContextPredMolecularEncoder(BaseMolecularEncoder):
22
+ """This encoder implements a GNN-based model for molecular representation learning
23
+ using the context prediction pretraining strategy.
24
+
25
+ References
26
+ ----------
27
+ - Paper: Strategies for Pre-training Graph Neural Networks (ICLR 2020) https://arxiv.org/abs/1905.12265
28
+ - Code: https://github.com/snap-stanford/pretrain-gnns/tree/master/chem
29
+
30
+ Parameters
31
+ ----------
32
+ mode : str, default="cbow"
33
+ Type of context prediction task. One of ["cbow", "skipgram"].
34
+ context_size : int, default=2
35
+ Size of the context window used for predicting node-level features.
36
+ neg_samples : int, default=1
37
+ Number of negative samples used in the training objective.
38
+ num_layer : int, default=3
39
+ Number of GNN layers.
40
+ hidden_size : int, default=300
41
+ Dimension of hidden node features.
42
+ drop_ratio : float, default=0.5
43
+ Dropout probability.
44
+ norm_layer : str, default="batch_norm"
45
+ Type of normalization layer to use. One of ["batch_norm", "layer_norm", "instance_norm", "graph_norm", "size_norm", "pair_norm"].
46
+ encoder_type : str, default="gin-virtual"
47
+ Type of GNN architecture to use. One of ["gin-virtual", "gcn-virtual", "gin", "gcn"].
48
+ readout : str, default="sum"
49
+ Method for aggregating node features to obtain graph-level representations. One of ["sum", "mean", "max"].
50
+ batch_size : int, default=128
51
+ Number of samples per batch for training.
52
+ epochs : int, default=500
53
+ Maximum number of training epochs.
54
+ learning_rate : float, default=0.001
55
+ Learning rate for the optimizer.
56
+ grad_clip_value : float, optional
57
+ Maximum norm of gradients for gradient clipping.
58
+ weight_decay : float, default=0.0
59
+ L2 regularization strength.
60
+ use_lr_scheduler : bool, default=False
61
+ Whether to use a learning rate scheduler during training.
62
+ scheduler_factor : float, default=0.5
63
+ Factor by which to reduce learning rate when plateau is reached.
64
+ scheduler_patience : int, default=5
65
+ Number of epochs with no improvement after which learning rate will be reduced.
66
+ verbose : bool, default=False
67
+ Whether to print progress information during training.
68
+ """
69
+ # Task related parameters
70
+ mode: str = "cbow" # cbow or skipgram
71
+ context_size: int = 2
72
+ neg_samples: int = 1
73
+
74
+ # Model parameters
75
+ num_layer: int = 3
76
+ hidden_size: int = 300
77
+ drop_ratio: float = 0.5
78
+ norm_layer: str = "batch_norm"
79
+
80
+ encoder_type: str = "gin-virtual"
81
+ readout: str = "sum"
82
+
83
+ # Training parameters
84
+ batch_size: int = 128
85
+ epochs: int = 500
86
+ learning_rate: float = 0.001
87
+ grad_clip_value: Optional[float] = None
88
+ weight_decay: float = 0.0
89
+
90
+ # Scheduler parameters
91
+ use_lr_scheduler: bool = False
92
+ scheduler_factor: float = 0.5
93
+ scheduler_patience: int = 5
94
+
95
+ # Other parameters
96
+ verbose: bool = False
97
+ model_name: str = "ContextPredMolecularEncoder"
98
+
99
+ # Non-init fields
100
+ fitting_loss: List[float] = field(default_factory=list, init=False)
101
+ fitting_epoch: int = field(default=0, init=False)
102
+ model_class: Type[GNN] = field(default=GNN, init=False)
103
+
104
+ def __post_init__(self):
105
+ """Initialize the model after dataclass initialization."""
106
+ super().__post_init__()
107
+ if self.encoder_type not in ALLOWABLE_ENCODER_MODELS:
108
+ raise ValueError(f"Invalid encoder_model: {self.encoder_type}. Currently only {ALLOWABLE_ENCODER_MODELS} are supported.")
109
+ if self.readout not in ALLOWABLE_ENCODER_READOUTS:
110
+ raise ValueError(f"Invalid encoder_readout: {self.readout}. Currently only {ALLOWABLE_ENCODER_READOUTS} are supported.")
111
+
112
+ @staticmethod
113
+ def _get_param_names() -> List[str]:
114
+ return ["mode", "context_size", "neg_samples"] + GNN_ENCODER_PARAMS.copy()
115
+
116
+ def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
117
+ params = {
118
+ "num_layer": self.num_layer,
119
+ "hidden_size": self.hidden_size,
120
+ "drop_ratio": self.drop_ratio,
121
+ "norm_layer": self.norm_layer,
122
+ "readout": self.readout,
123
+ "encoder_type": self.encoder_type,
124
+ "mode": self.mode,
125
+ "context_size": self.context_size,
126
+ "neg_samples": self.neg_samples
127
+ }
128
+ if checkpoint is not None:
129
+ if "hyperparameters" not in checkpoint:
130
+ raise ValueError("Checkpoint missing 'hyperparameters' key")
131
+ hyperparameters = checkpoint["hyperparameters"]
132
+ params = {k: hyperparameters.get(k, v) for k, v in params.items()}
133
+
134
+ return params
135
+
136
+ def _convert_to_pytorch_data(self, X):
137
+ """Convert numpy arrays to PyTorch Geometric data format.
138
+ """
139
+ if self.verbose:
140
+ iterator = tqdm(enumerate(X), desc="Converting molecules to graphs", total=len(X))
141
+ else:
142
+ iterator = enumerate(X)
143
+
144
+ pyg_graph_list = []
145
+ for idx, smiles_or_mol in iterator:
146
+ graph = graph_from_smiles(smiles_or_mol, None)
147
+ g = Data()
148
+ g.num_nodes = graph["num_nodes"]
149
+ g.edge_index = torch.from_numpy(graph["edge_index"])
150
+
151
+ del graph["num_nodes"]
152
+ del graph["edge_index"]
153
+
154
+ if graph["edge_feat"] is not None:
155
+ g.edge_attr = torch.from_numpy(graph["edge_feat"])
156
+ del graph["edge_feat"]
157
+
158
+ if graph["node_feat"] is not None:
159
+ g.x = torch.from_numpy(graph["node_feat"])
160
+ del graph["node_feat"]
161
+
162
+ pyg_graph_list.append(g)
163
+
164
+ return pyg_graph_list
165
+
166
+ def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
167
+ """Setup optimization components including optimizer and learning rate scheduler.
168
+ """
169
+
170
+ optimizer = torch.optim.Adam(self.model.parameters(), lr=self.learning_rate, weight_decay=self.weight_decay)
171
+ scheduler = None
172
+ if self.use_lr_scheduler:
173
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
174
+ optimizer,
175
+ mode="min",
176
+ factor=self.scheduler_factor,
177
+ patience=self.scheduler_patience,
178
+ min_lr=1e-6,
179
+ cooldown=0,
180
+ eps=1e-8,
181
+ )
182
+
183
+ return optimizer, scheduler
184
+
185
+ def fit(
186
+ self,
187
+ X_train: List[str]
188
+ ) -> "ContextPredMolecularEncoder":
189
+ """Fit the model to the training data with optional validation set.
190
+
191
+ Parameters
192
+ ----------
193
+ X_train : List[str]
194
+ Training set input molecular structures as SMILES strings
195
+ Returns
196
+ -------
197
+ self : ContextPredMolecularEncoder
198
+ Fitted estimator
199
+ """
200
+
201
+ self._initialize_model(self.model_class)
202
+ self.model.initialize_parameters()
203
+ optimizer, scheduler = self._setup_optimizers()
204
+
205
+ # Prepare datasets and loaders
206
+ X_train, _ = self._validate_inputs(X_train, return_rdkit_mol=True)
207
+ train_dataset = self._convert_to_pytorch_data(X_train)
208
+ train_loader = DataLoader(
209
+ train_dataset,
210
+ batch_size=self.batch_size,
211
+ shuffle=True,
212
+ num_workers=0
213
+ )
214
+ self.fitting_loss = []
215
+
216
+ for epoch in range(self.epochs):
217
+ # Training phase
218
+ train_losses = self._train_epoch(train_loader, optimizer, epoch)
219
+ self.fitting_loss.append(np.mean(train_losses))
220
+ if scheduler:
221
+ scheduler.step(np.mean(train_losses))
222
+
223
+ self.fitting_epoch = epoch
224
+ self.is_fitted_ = True
225
+ return self
226
+
227
+ def _train_epoch(self, train_loader, optimizer, epoch):
228
+ """Training logic for one epoch.
229
+
230
+ Args:
231
+ train_loader: DataLoader containing training data
232
+ optimizer: Optimizer instance for model parameter updates
233
+
234
+ Returns:
235
+ list: List of loss values for each training step
236
+ """
237
+ self.model.train()
238
+ losses = []
239
+
240
+ iterator = (
241
+ tqdm(train_loader, desc="Training", leave=False)
242
+ if self.verbose
243
+ else train_loader
244
+ )
245
+
246
+ for batch in iterator:
247
+ batch = batch.to(self.device)
248
+ optimizer.zero_grad()
249
+ loss = self.model.compute_loss(batch)
250
+ loss.backward()
251
+ if self.grad_clip_value is not None:
252
+ torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.grad_clip_value)
253
+ optimizer.step()
254
+ losses.append(loss.item())
255
+
256
+ # Update progress bar if using tqdm
257
+ if self.verbose:
258
+ iterator.set_postfix({"Epoch": f"{epoch}", "Loss": f"{loss.item():.4f}"})
259
+
260
+ return losses
261
+
262
+ def encode(self, X: List[str], return_type: Literal["np", "pt"] = "pt") -> Union[np.ndarray, torch.Tensor]:
263
+ """Encode molecules into vector representations.
264
+
265
+ Parameters
266
+ ----------
267
+ X : List[str]
268
+ List of SMILES strings
269
+ return_type : Literal["np", "pt"], default="pt"
270
+ Return type of the representations
271
+
272
+ Returns
273
+ -------
274
+ representations : ndarray or torch.Tensor
275
+ Molecular representations
276
+ """
277
+ self._check_is_fitted()
278
+
279
+ # Convert to PyTorch Geometric format and create loader
280
+ X, _ = self._validate_inputs(X, return_rdkit_mol=True)
281
+ dataset = self._convert_to_pytorch_data(X)
282
+ loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
283
+
284
+ if self.model is None:
285
+ raise RuntimeError("Model not initialized")
286
+
287
+ # Generate encodings
288
+ self.model = self.model.to(self.device)
289
+ self.model.eval()
290
+ encodings = []
291
+ with torch.no_grad():
292
+ for batch in tqdm(loader, disable=not self.verbose):
293
+ batch = batch.to(self.device)
294
+ out = self.model(batch)
295
+ encodings.append(out["graph"].cpu())
296
+
297
+ # Concatenate and convert to requested format
298
+ encodings = torch.cat(encodings, dim=0)
299
+ return encodings if return_type == "pt" else encodings.numpy()
@@ -0,0 +1,206 @@
1
+ import torch
2
+ import copy
3
+ import random
4
+ import networkx as nx
5
+ import numpy as np
6
+
7
+ from torch_geometric.data import Data
8
+
9
+ def cumsum(self, key, item):
10
+ r"""If :obj:`True`, the attribute :obj:`key` with content :obj:`item`
11
+ should be added up cumulatively before concatenated together.
12
+ .. note::
13
+ This method is for internal use only, and should only be overridden
14
+ if the batch concatenation process is corrupted for a specific data
15
+ attribute.
16
+ """
17
+ return key in ["edge_index", "edge_index_substruct", "edge_index_context", "overlap_context_substruct_idx", "center_substruct_idx"]
18
+
19
+ class ExtractSubstructureContextPair:
20
+ def __init__(self, k, l1, l2):
21
+ """
22
+ Randomly selects a node from the data object, and adds attributes
23
+ that contain the substructure that corresponds to k hop neighbours
24
+ rooted at the node, and the context substructures that corresponds to
25
+ the subgraph that is between l1 and l2 hops away from the
26
+ root node.
27
+ :param k:
28
+ :param l1:
29
+ :param l2:
30
+ """
31
+ self.k = k
32
+ self.l1 = l1
33
+ self.l2 = l2
34
+
35
+ # for the special case of 0, addresses the quirk with
36
+ # single_source_shortest_path_length
37
+ if self.k == 0:
38
+ self.k = -1
39
+ if self.l1 == 0:
40
+ self.l1 = -1
41
+ if self.l2 == 0:
42
+ self.l2 = -1
43
+
44
+ def __call__(self, data, root_idx=None):
45
+ """
46
+
47
+ :param data: pytorch geometric data object
48
+ :param root_idx: If None, then randomly samples an atom idx.
49
+ Otherwise sets atom idx of root (for debugging only)
50
+ :return: None. Creates new attributes in original data object:
51
+ data.center_substruct_idx
52
+ data.x_substruct
53
+ data.edge_attr_substruct
54
+ data.edge_index_substruct
55
+ data.x_context
56
+ data.edge_attr_context
57
+ data.edge_index_context
58
+ data.overlap_context_substruct_idx
59
+ """
60
+ num_atoms = data.x.size()[0]
61
+ if root_idx == None:
62
+ root_idx = random.sample(range(num_atoms), 1)[0]
63
+
64
+ G = graph_data_obj_to_nx_simple(data) # same ordering as input data obj
65
+
66
+ # Get k-hop subgraph rooted at specified atom idx
67
+ substruct_node_idxes = nx.single_source_shortest_path_length(G,
68
+ root_idx,
69
+ self.k).keys()
70
+ if len(substruct_node_idxes) > 0:
71
+ substruct_G = G.subgraph(substruct_node_idxes)
72
+ substruct_G, substruct_node_map = reset_idxes(substruct_G) # need
73
+ # to reset node idx to 0 -> num_nodes - 1, otherwise data obj does not
74
+ # make sense, since the node indices in data obj must start at 0
75
+ substruct_data = nx_to_graph_data_obj_simple(substruct_G)
76
+ data.substruct_node_idxes = torch.tensor(list(substruct_node_idxes))
77
+ data.x_substruct = substruct_data.x
78
+ data.edge_attr_substruct = substruct_data.edge_attr
79
+ data.edge_index_substruct = substruct_data.edge_index
80
+ data.center_substruct_idx = torch.tensor([substruct_node_map[
81
+ root_idx]]) # need
82
+ # to convert center idx from original graph node ordering to the
83
+ # new substruct node ordering
84
+
85
+ # Get subgraphs that is between l1 and l2 hops away from the root node
86
+ l1_node_idxes = nx.single_source_shortest_path_length(G, root_idx,
87
+ self.l1).keys()
88
+ l2_node_idxes = nx.single_source_shortest_path_length(G, root_idx,
89
+ self.l2).keys()
90
+ context_node_idxes = set(l1_node_idxes).symmetric_difference(
91
+ set(l2_node_idxes))
92
+ if len(context_node_idxes) > 0:
93
+ context_G = G.subgraph(context_node_idxes)
94
+ context_G, context_node_map = reset_idxes(context_G) # need to
95
+ # reset node idx to 0 -> num_nodes - 1, otherwise data obj does not
96
+ # make sense, since the node indices in data obj must start at 0
97
+ context_data = nx_to_graph_data_obj_simple(context_G)
98
+ data.context_node_idxes = torch.tensor(list(context_node_idxes))
99
+ data.x_context = context_data.x
100
+ data.edge_attr_context = context_data.edge_attr
101
+ data.edge_index_context = context_data.edge_index
102
+
103
+ # Get indices of overlapping nodes between substruct and context,
104
+ # WRT context ordering
105
+ context_substruct_overlap_idxes = list(set(
106
+ context_node_idxes).intersection(set(substruct_node_idxes)))
107
+ if len(context_substruct_overlap_idxes) > 0:
108
+ context_substruct_overlap_idxes_reorder = [context_node_map[old_idx]
109
+ for
110
+ old_idx in
111
+ context_substruct_overlap_idxes]
112
+ # need to convert the overlap node idxes, which is from the
113
+ # original graph node ordering to the new context node ordering
114
+ data.overlap_context_substruct_idx = \
115
+ torch.tensor(context_substruct_overlap_idxes_reorder)
116
+
117
+ return data
118
+
119
+ def reset_idxes(G):
120
+ """
121
+ Resets node indices such that they are numbered from 0 to num_nodes - 1
122
+ :param G:
123
+ :return: copy of G with relabelled node indices, mapping
124
+ """
125
+ mapping = {}
126
+ for new_idx, old_idx in enumerate(G.nodes()):
127
+ mapping[old_idx] = new_idx
128
+ new_G = nx.relabel_nodes(G, mapping, copy=True)
129
+ return new_G, mapping
130
+
131
+ def graph_data_obj_to_nx_simple(data):
132
+ """
133
+ Converts graph Data object required by the pytorch geometric package to
134
+ network x data object. NB: Uses simplified atom and bond features,
135
+ and represent as indices. NB: possible issues with recapitulating relative
136
+ stereochemistry since the edges in the nx object are unordered.
137
+ :param data: pytorch geometric Data object
138
+ :return: network x object
139
+ """
140
+ G = nx.Graph()
141
+
142
+ # atoms
143
+ atom_features = data.x.cpu().numpy()
144
+ num_atoms = atom_features.shape[0]
145
+ for i in range(num_atoms):
146
+ atomic_num_idx, chirality_tag_idx, _, _, _, _, _, _, _ = atom_features[i]
147
+ G.add_node(i, atom_num_idx=atomic_num_idx, chirality_tag_idx=chirality_tag_idx)
148
+ pass
149
+
150
+ # bonds
151
+ edge_index = data.edge_index.cpu().numpy()
152
+ edge_attr = data.edge_attr.cpu().numpy()
153
+ num_bonds = edge_index.shape[1]
154
+ for j in range(0, num_bonds, 2):
155
+ begin_idx = int(edge_index[0, j])
156
+ end_idx = int(edge_index[1, j])
157
+ bond_type_idx, bond_dir_idx, _ = edge_attr[j]
158
+ if not G.has_edge(begin_idx, end_idx):
159
+ G.add_edge(begin_idx, end_idx, bond_type_idx=bond_type_idx,
160
+ bond_dir_idx=bond_dir_idx)
161
+
162
+ return G
163
+
164
+ def nx_to_graph_data_obj_simple(G):
165
+ """
166
+ Converts nx graph to pytorch geometric Data object. Assume node indices
167
+ are numbered from 0 to num_nodes - 1. NB: Uses simplified atom and bond
168
+ features, and represent as indices. NB: possible issues with
169
+ recapitulating relative stereochemistry since the edges in the nx
170
+ object are unordered.
171
+ :param G: nx graph obj
172
+ :return: pytorch geometric Data object
173
+ """
174
+ # atoms
175
+ num_atom_features = 2 # atom type, chirality tag
176
+ atom_features_list = []
177
+ for _, node in G.nodes(data=True):
178
+ atom_feature = [node['atom_num_idx'], node['chirality_tag_idx']]
179
+ atom_features_list.append(atom_feature)
180
+ x = torch.tensor(np.array(atom_features_list), dtype=torch.long)
181
+
182
+ # bonds
183
+ num_bond_features = 2 # bond type, bond direction
184
+ if len(G.edges()) > 0: # mol has bonds
185
+ edges_list = []
186
+ edge_features_list = []
187
+ for i, j, edge in G.edges(data=True):
188
+ edge_feature = [edge['bond_type_idx'], edge['bond_dir_idx']]
189
+ edges_list.append((i, j))
190
+ edge_features_list.append(edge_feature)
191
+ edges_list.append((j, i))
192
+ edge_features_list.append(edge_feature)
193
+
194
+ # data.edge_index: Graph connectivity in COO format with shape [2, num_edges]
195
+ edge_index = torch.tensor(np.array(edges_list).T, dtype=torch.long)
196
+
197
+ # data.edge_attr: Edge feature matrix with shape [num_edges, num_edge_features]
198
+ edge_attr = torch.tensor(np.array(edge_features_list),
199
+ dtype=torch.long)
200
+ else: # mol has no bonds
201
+ edge_index = torch.empty((2, 0), dtype=torch.long)
202
+ edge_attr = torch.empty((0, num_bond_features), dtype=torch.long)
203
+
204
+ data = Data(x=x, edge_index=edge_index, edge_attr=edge_attr)
205
+
206
+ return data
@@ -0,0 +1,3 @@
1
+ from .modeling_edgepred import EdgePredMolecularEncoder
2
+
3
+ __all__ = ['EdgePredMolecularEncoder']
@@ -0,0 +1,104 @@
1
+ import torch
2
+ import torch.nn as nn
3
+ from torch_geometric.nn import global_add_pool, global_mean_pool, global_max_pool
4
+
5
+ from ...nn import GNN_node, GNN_node_Virtualnode
6
+ from ...utils import init_weights
7
+
8
+ criterion = torch.nn.BCEWithLogitsLoss()
9
+
10
+ class GNN(nn.Module):
11
+ def __init__(
12
+ self,
13
+ num_layer,
14
+ hidden_size,
15
+ drop_ratio=0.5,
16
+ norm_layer="batch_norm",
17
+ encoder_type="gin-virtual",
18
+ readout="max"
19
+ ):
20
+ super(GNN, self).__init__()
21
+ gnn_name = encoder_type.split("-")[0]
22
+ self.hidden_size = hidden_size
23
+ encoder_params = {
24
+ "num_layer": num_layer,
25
+ "hidden_size": hidden_size,
26
+ "JK": "last",
27
+ "drop_ratio": drop_ratio,
28
+ "residual": True,
29
+ "gnn_name": gnn_name,
30
+ "norm_layer": norm_layer
31
+ }
32
+
33
+ # Choose encoder type based on encoder_model
34
+ encoder_class = GNN_node_Virtualnode if "virtual" in encoder_type else GNN_node
35
+ self.graph_encoder = encoder_class(**encoder_params)
36
+ pooling_funcs = {
37
+ "sum": global_add_pool,
38
+ "mean": global_mean_pool,
39
+ "max": global_max_pool
40
+ }
41
+ self.pool = pooling_funcs.get(readout)
42
+ if self.pool is None:
43
+ raise ValueError(f"Invalid graph pooling type {readout}.")
44
+
45
+ def initialize_parameters(self, seed=None):
46
+ """
47
+ Randomly initialize all model parameters using the init_weights function.
48
+
49
+ Args:
50
+ seed (int, optional): Random seed for reproducibility. Defaults to None.
51
+ """
52
+ if seed is not None:
53
+ torch.manual_seed(seed)
54
+
55
+ # Initialize the main components
56
+ init_weights(self.graph_encoder)
57
+
58
+ # Reset all parameters using PyTorch Geometric's reset function
59
+ def reset_parameters(module):
60
+ if hasattr(module, 'reset_parameters'):
61
+ module.reset_parameters()
62
+ elif hasattr(module, 'weight') and hasattr(module.weight, 'data'):
63
+ init_weights(module)
64
+
65
+ self.apply(reset_parameters)
66
+
67
+ def compute_loss(self, batched_data):
68
+ # sample negative edges
69
+ num_nodes = batched_data.num_nodes
70
+ num_edges = batched_data.num_edges
71
+
72
+ edge_set = set([str(batched_data.edge_index[0, i].cpu().item()) + "," + str(
73
+ batched_data.edge_index[1, i].cpu().item()) for i in range(batched_data.edge_index.shape[1])])
74
+
75
+ redandunt_sample = torch.randint(0, num_nodes, (2, 5 * num_edges))
76
+ sampled_ind = []
77
+ sampled_edge_set = set([])
78
+ for i in range(5 * num_edges):
79
+ node1 = redandunt_sample[0, i].cpu().item()
80
+ node2 = redandunt_sample[1, i].cpu().item()
81
+ edge_str = str(node1) + "," + str(node2)
82
+ if not edge_str in edge_set and not edge_str in sampled_edge_set and not node1 == node2:
83
+ sampled_edge_set.add(edge_str)
84
+ sampled_ind.append(i)
85
+ if len(sampled_ind) == num_edges / 2:
86
+ break
87
+
88
+ batched_data.negative_edge_index = redandunt_sample[:, sampled_ind]
89
+
90
+ # generate predictions
91
+ h_node, _ = self.graph_encoder(batched_data)
92
+ h_rep = self.pool(h_node, batched_data.batch)
93
+
94
+ positive_score = torch.sum(h_node[batched_data.edge_index[0, ::2]] * h_node[batched_data.edge_index[1, ::2]], dim = 1)
95
+ negative_score = torch.sum(h_node[batched_data.negative_edge_index[0]] * h_node[batched_data.negative_edge_index[1]], dim = 1)
96
+
97
+ loss_class = criterion(positive_score, torch.ones_like(positive_score)) + criterion(negative_score, torch.zeros_like(negative_score))
98
+
99
+ return loss_class
100
+
101
+ def forward(self, batched_data):
102
+ h_node, _ = self.graph_encoder(batched_data)
103
+ h_rep = self.pool(h_node, batched_data.batch)
104
+ return {"graph": h_rep, "node": h_node}