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,252 @@
1
+ import os
2
+ import numpy as np
3
+ from tqdm import tqdm
4
+ from typing import Optional, Union, Dict, Any, List, Callable, Type, Tuple
5
+ import warnings
6
+ from dataclasses import dataclass, field
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+ import torch.optim as optim
11
+ from torch.utils.data import DataLoader, TensorDataset
12
+
13
+ from .model import Transformer
14
+ from ..lstm.modeling_lstm import LSTMMolecularPredictor
15
+ from ...utils.search import (
16
+ suggest_parameter,
17
+ ParameterSpec,
18
+ ParameterType,
19
+ )
20
+
21
+ # Dictionary mapping parameter names to their types and ranges
22
+ DEFAULT_TRANSFORMER_SEARCH_SPACES: Dict[str, ParameterSpec] = {
23
+ # Integer-valued parameters
24
+ "hidden_size": ParameterSpec(ParameterType.INTEGER, (64, 256)),
25
+ "n_heads": ParameterSpec(ParameterType.INTEGER, (2, 8)),
26
+ "num_layers": ParameterSpec(ParameterType.INTEGER, (2, 6)),
27
+ "dim_feedforward": ParameterSpec(ParameterType.INTEGER, (128, 512)),
28
+ # Float-valued parameters with log scale
29
+ "learning_rate": ParameterSpec(ParameterType.LOG_FLOAT, (1e-5, 1e-2)),
30
+ "weight_decay": ParameterSpec(ParameterType.LOG_FLOAT, (1e-8, 1e-3)),
31
+ "dropout": ParameterSpec(ParameterType.FLOAT, (0.0, 0.5)),
32
+ "scheduler_factor": ParameterSpec(ParameterType.FLOAT, (0.1, 0.5)),
33
+ }
34
+
35
+ @dataclass
36
+ class SMILESTransformerMolecularPredictor(LSTMMolecularPredictor):
37
+ """This predictor implements a Transformer model for SMILES-based molecular property predictions.
38
+
39
+ Notes
40
+ -----
41
+ This implementation uses a transformer encoder architecture to learn
42
+ representations of molecular structures from SMILES strings.
43
+
44
+ Parameters
45
+ ----------
46
+ num_task : int, default=1
47
+ Number of prediction tasks.
48
+ task_type : str, default="regression"
49
+ Type of prediction task, either "regression" or "classification".
50
+ input_dim : int, default=54
51
+ Size of vocabulary for SMILES tokenization.
52
+ hidden_size : int, default=128
53
+ Dimension of embedding vectors.
54
+ n_heads : int, default=4
55
+ Number of attention heads in transformer layers.
56
+ num_layers : int, default=3
57
+ Number of transformer encoder layers.
58
+ dim_feedforward : int, default=256
59
+ Dimension of the feedforward network in transformer layers.
60
+ max_input_len : int, default=200
61
+ Maximum length of input sequences. Shorter sequences will be padded.
62
+ dropout : float, default=0.1
63
+ Dropout rate for transformer layers.
64
+ batch_size : int, default=64
65
+ Number of samples per batch for training.
66
+ epochs : int, default=200
67
+ Maximum number of training epochs.
68
+ loss_criterion : callable, optional
69
+ Loss function for training. Defaults to MSELoss for regression.
70
+ evaluate_criterion : str or callable, optional
71
+ Metric for model evaluation.
72
+ evaluate_higher_better : bool, optional
73
+ Whether higher values of the evaluation metric are better.
74
+ learning_rate : float, default=0.0001
75
+ Learning rate for optimizer.
76
+ weight_decay : float, default=0.0
77
+ L2 regularization strength.
78
+ patience : int, default=20
79
+ Number of epochs to wait for improvement before early stopping.
80
+ use_lr_scheduler : bool, default=True
81
+ Whether to use learning rate scheduler.
82
+ scheduler_factor : float, default=0.5
83
+ Factor by which to reduce learning rate when plateau is reached.
84
+ scheduler_patience : int, default=5
85
+ Number of epochs with no improvement after which learning rate will be reduced.
86
+ verbose : bool, default=False
87
+ Whether to print progress information during training.
88
+ """
89
+ # Model parameters
90
+ num_task: int = 1
91
+ task_type: str = "regression"
92
+ input_dim: int = 54 # vocabulary size
93
+ hidden_size: int = 128
94
+ n_heads: int = 4
95
+ num_layers: int = 3
96
+ dim_feedforward: Optional[int] = 256
97
+ max_input_len: int = 200 # max token length
98
+ dropout: float = 0.1
99
+
100
+ # Training parameters
101
+ batch_size: int = 64
102
+ epochs: int = 200
103
+ loss_criterion: Optional[Callable] = None
104
+ evaluate_criterion: Optional[Union[str, Callable]] = None
105
+ evaluate_higher_better: Optional[bool] = None
106
+ learning_rate: float = 0.0001
107
+ weight_decay: float = 0.0
108
+ patience: int = 20
109
+
110
+ # Scheduler parameters
111
+ use_lr_scheduler: bool = True
112
+ scheduler_factor: float = 0.5
113
+ scheduler_patience: int = 5
114
+
115
+ # Other parameters
116
+ verbose: bool = False
117
+ model_name: str = "SMILESTransformerMolecularPredictor"
118
+
119
+ # Non-init fields
120
+ fitting_loss: List[float] = field(default_factory=list, init=False)
121
+ fitting_epoch: int = field(default=0, init=False)
122
+ model_class: Type[Transformer] = field(default=Transformer, init=False)
123
+
124
+ def __post_init__(self):
125
+ """Initialize after dataclass initialization."""
126
+ super().__post_init__()
127
+ # Validate n_heads is compatible with hidden_size
128
+ if self.hidden_size % self.n_heads != 0:
129
+ raise ValueError(f"hidden_size ({self.hidden_size}) must be divisible by n_heads ({self.n_heads})")
130
+
131
+ # Setup loss criterion and evaluation
132
+ if self.loss_criterion is None:
133
+ self.loss_criterion = nn.MSELoss()
134
+ self._setup_evaluation(self.evaluate_criterion, self.evaluate_higher_better)
135
+
136
+ @staticmethod
137
+ def _get_param_names() -> List[str]:
138
+ """Get parameter names for the estimator.
139
+
140
+ Returns
141
+ -------
142
+ List[str]
143
+ List of parameter names that can be used for model configuration.
144
+ """
145
+ return [
146
+ # Model Hyperparameters
147
+ "num_task",
148
+ "task_type",
149
+ "input_dim",
150
+ "hidden_size",
151
+ "n_heads",
152
+ "num_layers",
153
+ "dim_feedforward",
154
+ "max_input_len",
155
+ "dropout",
156
+ # Training Parameters
157
+ "batch_size",
158
+ "epochs",
159
+ "learning_rate",
160
+ "weight_decay",
161
+ "patience",
162
+ "loss_criterion",
163
+ # Evaluation Parameters
164
+ "evaluate_name",
165
+ "evaluate_criterion",
166
+ "evaluate_higher_better",
167
+ # Scheduler Parameters
168
+ "use_lr_scheduler",
169
+ "scheduler_factor",
170
+ "scheduler_patience",
171
+ # Other Parameters
172
+ "fitting_epoch",
173
+ "fitting_loss",
174
+ "device",
175
+ "verbose"
176
+ ]
177
+
178
+ def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
179
+ """Get model parameters from checkpoint or current instance.
180
+
181
+ Parameters
182
+ ----------
183
+ checkpoint : Optional[Dict], default=None
184
+ Optional dictionary containing model checkpoint data
185
+
186
+ Returns
187
+ -------
188
+ Dict[str, Any]
189
+ Dictionary of model parameters
190
+ """
191
+ if checkpoint is not None:
192
+ if "hyperparameters" not in checkpoint:
193
+ raise ValueError("Checkpoint missing 'hyperparameters' key")
194
+
195
+ hyperparameters = checkpoint["hyperparameters"]
196
+
197
+
198
+ return {
199
+ "num_task": hyperparameters.get("num_task", self.num_task),
200
+ "input_dim": hyperparameters.get("input_dim", self.input_dim),
201
+ "hidden_size": hyperparameters.get("hidden_size", self.hidden_size),
202
+ "n_heads": hyperparameters.get("n_heads", self.n_heads),
203
+ "num_layers": hyperparameters.get("num_layers", self.num_layers),
204
+ "dim_feedforward": hyperparameters.get("dim_feedforward", self.dim_feedforward),
205
+ "max_input_len": hyperparameters.get("max_input_len", self.max_input_len),
206
+ "dropout": hyperparameters.get("dropout", self.dropout),
207
+ }
208
+ else:
209
+ return {
210
+ "num_task": self.num_task,
211
+ "input_dim": self.input_dim,
212
+ "hidden_size": self.hidden_size,
213
+ "n_heads": self.n_heads,
214
+ "num_layers": self.num_layers,
215
+ "dim_feedforward": self.dim_feedforward,
216
+ "max_input_len": self.max_input_len,
217
+ "dropout": self.dropout,
218
+ }
219
+
220
+ def _setup_optimizers(self) -> Tuple[torch.optim.Optimizer, Optional[Any]]:
221
+ """Setup optimization components including optimizer and learning rate scheduler.
222
+
223
+ Returns
224
+ -------
225
+ Tuple[optim.Optimizer, Optional[Any]]
226
+ A tuple containing:
227
+ - The configured optimizer
228
+ - The learning rate scheduler (if enabled, else None)
229
+ """
230
+ optimizer = torch.optim.AdamW(
231
+ self.model.parameters(),
232
+ lr=self.learning_rate,
233
+ weight_decay=self.weight_decay
234
+ )
235
+
236
+ scheduler = None
237
+ if self.use_lr_scheduler:
238
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
239
+ optimizer,
240
+ mode='min' if not self.evaluate_higher_better else 'max',
241
+ factor=self.scheduler_factor,
242
+ patience=self.scheduler_patience,
243
+ verbose=self.verbose
244
+ )
245
+
246
+ return optimizer, scheduler
247
+
248
+
249
+ def _get_default_search_space(self):
250
+ """Get the default hyperparameter search space.
251
+ """
252
+ return DEFAULT_TRANSFORMER_SEARCH_SPACES
@@ -0,0 +1,3 @@
1
+ from .modeling_ssr import SSRMolecularPredictor
2
+
3
+ __all__ = ['SSRMolecularPredictor']
@@ -0,0 +1,283 @@
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
+ import copy
5
+ # from torch_scatter import scatter
6
+ from .utils import scatter
7
+
8
+ from ...nn import GNN_node, GNN_node_Virtualnode, MLP
9
+ from ...utils import init_weights
10
+
11
+ class SSR(nn.Module):
12
+ def __init__(
13
+ self,
14
+ num_task,
15
+ num_layer,
16
+ coarse_pool='mean',
17
+ hidden_size=300,
18
+ gnn_type="gin-virtual",
19
+ drop_ratio=0.5,
20
+ norm_layer="batch_norm",
21
+ graph_pooling="mean",
22
+ augmented_feature=['maccs', 'morgan']
23
+ ):
24
+
25
+ super(SSR, self).__init__()
26
+ gnn_name = gnn_type.split("-")[0]
27
+ self.num_task = num_task
28
+ self.hidden_size = hidden_size
29
+ self.coarse_pool = coarse_pool
30
+
31
+ if "virtual" in gnn_type:
32
+ self.graph_encoder = GNN_node_Virtualnode(
33
+ num_layer,
34
+ hidden_size,
35
+ JK="last",
36
+ drop_ratio=drop_ratio,
37
+ residual=True,
38
+ gnn_name=gnn_name,
39
+ norm_layer=norm_layer,
40
+ )
41
+ else:
42
+ self.graph_encoder = GNN_node(
43
+ num_layer,
44
+ hidden_size,
45
+ JK="last",
46
+ drop_ratio=drop_ratio,
47
+ residual=True,
48
+ gnn_name=gnn_name,
49
+ norm_layer=norm_layer,
50
+ )
51
+ if graph_pooling == "sum":
52
+ self.pool = global_add_pool
53
+ elif graph_pooling == "mean":
54
+ self.pool = global_mean_pool
55
+ elif graph_pooling == "max":
56
+ self.pool = global_max_pool
57
+ else:
58
+ raise ValueError(f"Invalid graph pooling type {graph_pooling}.")
59
+
60
+ graph_dim = hidden_size
61
+ self.augmented_feature = augmented_feature
62
+ if augmented_feature:
63
+ if "morgan" in augmented_feature:
64
+ graph_dim += 1024
65
+ if "maccs" in augmented_feature:
66
+ graph_dim += 167
67
+ self.predictor = MLP(graph_dim, hidden_features=2 * hidden_size, out_features=num_task)
68
+
69
+ def l2diff(self, x1, x2):
70
+ """Compute L2 difference between tensors"""
71
+ return torch.sqrt(torch.sum((x1 - x2) ** 2, dim=1))
72
+
73
+ def moment_diff(self, sx1, sx2, k, og_batch, coarse_batch):
74
+ """Compute difference between k-th moments"""
75
+ ss1 = scatter(sx1**k, og_batch, dim=0, reduce='mean')
76
+ ss2 = scatter(sx2**k, coarse_batch, dim=0, reduce='mean')
77
+ return self.l2diff(ss1, ss2)
78
+
79
+ def cmd(self, x1, x2, og_batch, coarse_batch,n_moments):
80
+ """Central Moment Discrepancy between x1 and x2 distributions"""
81
+ # Compute means
82
+ mx1 = scatter(x1, og_batch, dim=0, reduce='mean')
83
+ mx2 = scatter(x2, coarse_batch, dim=0, reduce='mean')
84
+ # Center features
85
+ sx1 = x1 - mx1[og_batch]
86
+ sx2 = x2 - mx2[coarse_batch]
87
+
88
+ # First moment difference
89
+ dm = self.l2diff(mx1, mx2)
90
+ scms = dm
91
+
92
+ # Higher order moments
93
+ for i in range(n_moments-1):
94
+ scms = scms + self.moment_diff(sx1, sx2, i+2, og_batch, coarse_batch)
95
+
96
+ return scms.mean()
97
+
98
+ def pool_batch_by_graph(self, clusters, original_batch):
99
+ """
100
+ Build a new batch vector for coarse nodes so that all coarse nodes derived from the same
101
+ original graph get the same graph label. Here, we iterate graph-by-graph.
102
+
103
+ Args:
104
+ clusters (Tensor): Cluster assignments for each original node, shape [num_original_nodes].
105
+ original_batch (Tensor): Original batch vector (graph assignments) for each node.
106
+
107
+ Returns:
108
+ Tensor: New batch vector for coarse nodes, of shape [num_coarse_nodes].
109
+ """
110
+ unique_graphs = torch.unique(original_batch)
111
+ new_batch_list = []
112
+ for g in unique_graphs:
113
+ # Get indices for nodes belonging to graph g.
114
+ mask = (original_batch == g)
115
+ # For these nodes, get their cluster ids.
116
+ clusters_in_g = clusters[mask]
117
+ # Determine the unique clusters in this graph.
118
+ unique_clusters_in_g = torch.unique(clusters_in_g)
119
+ # For each coarse node from graph g, assign the same graph id (g.item())
120
+ new_batch_list.extend([g.item()] * unique_clusters_in_g.numel())
121
+ return torch.tensor(new_batch_list, device=original_batch.device, dtype=torch.long)
122
+
123
+
124
+ def prepare_coarsened_batch(self, batch, ratio):
125
+ """
126
+ Create a coarsened version of batch for forward pass with edge attributes,
127
+ ensuring that the new batch assignment preserves the original graph labels.
128
+ """
129
+ new_batch = copy.deepcopy(batch)
130
+ coarse_ratio_postfix = str(int(ratio * 100))
131
+
132
+ # 1. Get clusters (one per original node) and ensure long type.
133
+ clusters = getattr(batch, f"clusters_{coarse_ratio_postfix}").long() # shape: [num_nodes]
134
+
135
+ # Compute a factor to disambiguate clusters from different graphs.
136
+ factor = clusters.max().item() + 1 # All clusters are in [0, factor-1]
137
+ # Create a global key that combines the original batch assignment and clusters.
138
+ global_coarse_key = batch.batch * factor + clusters # shape: [num_nodes]
139
+
140
+ # Get the unique global keys and the inverse mapping.
141
+ unique_global_keys, inverse_indices = torch.unique(global_coarse_key, return_inverse=True)
142
+ num_coarse_nodes = unique_global_keys.numel()
143
+
144
+ # 2. Pool node features using the global coarse key.
145
+ new_x = scatter(batch.x, inverse_indices, dim=0, reduce=self.coarse_pool, dim_size=num_coarse_nodes)
146
+ new_batch.x = new_x
147
+
148
+ # 3. Build the new batch vector for coarse nodes:
149
+ # The graph assignment for each coarse node is simply:
150
+ # graph_id = unique_global_key // factor
151
+ new_batch_vec = unique_global_keys // factor
152
+ new_batch.batch = new_batch_vec
153
+
154
+ # 4. Process the coarsened edge index.
155
+ # Assume that the stored coarsened edge index is based on original node indices.
156
+ # Map each node index to its global coarse key.
157
+ coarse_edge_index = getattr(batch, f"coarsened_edge_index_{coarse_ratio_postfix}")
158
+ # For each index in coarse_edge_index, replace with its global coarse key.
159
+ temp_edge_index = torch.stack([
160
+ global_coarse_key[coarse_edge_index[0]],
161
+ global_coarse_key[coarse_edge_index[1]]
162
+ ])
163
+ # Build a mapping from global key to new index from unique_global_keys.
164
+ mapping = {old.item(): new for new, old in enumerate(unique_global_keys)}
165
+ new_edge_index = torch.zeros_like(temp_edge_index)
166
+ for i in range(temp_edge_index.size(1)):
167
+ new_edge_index[0, i] = mapping[temp_edge_index[0, i].item()]
168
+ new_edge_index[1, i] = mapping[temp_edge_index[1, i].item()]
169
+ new_batch.edge_index = new_edge_index
170
+
171
+ # 5. Process edge attributes, if available.
172
+ if hasattr(batch, f"coarsened_edge_attr_{coarse_ratio_postfix}"):
173
+ edge_attr = getattr(batch, f"coarsened_edge_attr_{coarse_ratio_postfix}")
174
+ new_batch.edge_attr = torch.round(edge_attr).long()
175
+
176
+ new_batch.num_nodes = num_coarse_nodes
177
+ '''
178
+ # Debug prints:
179
+ print("\n=== Feature and Cluster Info ===")
180
+ print(f"Original x shape: {batch.x.shape}")
181
+ print(f"Original batch shape: {batch.batch.shape}")
182
+ print(f"Number of unique clusters (globally disambiguated): {num_coarse_nodes}")
183
+ print("\n=== Final Dimensions ===")
184
+ print(f"New x shape: {new_batch.x.shape}") # Should be [num_coarse_nodes, feature_dim]
185
+ print(f"New batch shape: {new_batch.batch.shape}") # Should be [num_coarse_nodes]
186
+ print(f"New edge_index shape: {new_batch.edge_index.shape}")
187
+ '''
188
+ # Validate dimensions.
189
+ assert new_batch.batch.numel() == num_coarse_nodes, \
190
+ f"Batch size {new_batch.batch.numel()} doesn't match number of coarse nodes {num_coarse_nodes}"
191
+ assert new_batch.x.shape[0] == num_coarse_nodes, \
192
+ f"Number of node features {new_batch.x.shape[0]} doesn't match number of coarse nodes {num_coarse_nodes}"
193
+
194
+ return new_batch
195
+
196
+
197
+
198
+ def initialize_parameters(self, seed=None):
199
+ """
200
+ Randomly initialize all model parameters using the init_weights function.
201
+
202
+ Args:
203
+ seed (int, optional): Random seed for reproducibility. Defaults to None.
204
+ """
205
+ if seed is not None:
206
+ torch.manual_seed(seed)
207
+
208
+ # Initialize the main components
209
+ init_weights(self.graph_encoder)
210
+ init_weights(self.predictor)
211
+
212
+ # Reset all parameters using PyTorch Geometric's reset function
213
+ def reset_parameters(module):
214
+ if hasattr(module, 'reset_parameters'):
215
+ module.reset_parameters()
216
+ elif hasattr(module, 'weight') and hasattr(module.weight, 'data'):
217
+ init_weights(module)
218
+
219
+ self.apply(reset_parameters)
220
+
221
+ def _augmented_graph_features(self, batched_data, h_rep):
222
+ if self.augmented_feature:
223
+ if 'morgan' in self.augmented_feature:
224
+ morgan = batched_data.morgan.type_as(h_rep)
225
+ h_rep = torch.cat((h_rep, morgan), dim=1)
226
+ if 'maccs' in self.augmented_feature:
227
+ maccs = batched_data.maccs.type_as(h_rep)
228
+ h_rep = torch.cat((h_rep, maccs), dim=1)
229
+ return h_rep
230
+
231
+
232
+ def compute_loss(self, batched_data, criterion, coarse_ratios=[0.8, 0.9],cmd_coeff=0.1,fine_grained=True,n_moments=5):
233
+ """Compute loss with SSR regularization"""
234
+ # Original forward pass
235
+ h_node, _ = self.graph_encoder(batched_data)
236
+ h_rep = self.pool(h_node, batched_data.batch)
237
+ h_rep = self._augmented_graph_features(batched_data, h_rep)
238
+ prediction = self.predictor(h_rep)
239
+
240
+ # Compute prediction loss
241
+ target = batched_data.y.to(torch.float32)
242
+ is_labeled = batched_data.y == batched_data.y # Check for NaN
243
+ pred_loss = criterion(prediction.to(torch.float32)[is_labeled], target[is_labeled]).mean()
244
+
245
+ # Initialize SSR loss
246
+ ssr_loss = torch.tensor(0.0, device=batched_data.x.device)
247
+
248
+ # Process each coarsening ratio for SSR
249
+ for ratio in coarse_ratios:
250
+ # Skip if coarsened data wasn't pre-computed
251
+ if not hasattr(batched_data, f"coarsened_edge_index_{str(int(ratio*100))}"):
252
+ continue
253
+
254
+ # Prepare coarsened batch
255
+ coarse_batch = self.prepare_coarsened_batch(batched_data, ratio)
256
+
257
+ # Get node embeddings for coarsened graph
258
+ coarse_h_node, _ = self.graph_encoder(coarse_batch)
259
+
260
+ # Compute CMD loss
261
+ if fine_grained:
262
+ # Node-level CMD
263
+ ssr_loss = ssr_loss + self.cmd(h_node,
264
+ coarse_h_node,
265
+ batched_data.batch,
266
+ coarse_batch.batch,n_moments)
267
+ else:
268
+ # Graph-level CMD
269
+ coarse_h_rep = self.pool(coarse_h_node, coarse_batch.batch)
270
+ ssr_loss = ssr_loss + torch.norm(h_rep - coarse_h_rep, dim=1).mean()
271
+
272
+ # Compute total loss
273
+ total_loss = pred_loss + cmd_coeff * ssr_loss
274
+
275
+ return total_loss, pred_loss, ssr_loss
276
+
277
+ def forward(self, batched_data):
278
+ """Standard forward pass (for inference without SSR)"""
279
+ h_node, _ = self.graph_encoder(batched_data)
280
+ h_rep = self.pool(h_node, batched_data.batch)
281
+ h_rep = self._augmented_graph_features(batched_data, h_rep)
282
+ prediction = self.predictor(h_rep)
283
+ return {"prediction": prediction}