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,310 @@
1
+ import numpy as np
2
+ import random
3
+ import joblib
4
+ from joblib import delayed
5
+ from rdkit import Chem
6
+ from tqdm import tqdm
7
+ from typing import Optional, Union, Dict, Any, Tuple, List, Callable
8
+ from dataclasses import dataclass, field
9
+ from warnings import warn
10
+
11
+ from .crossover import crossover
12
+ from .mutate import mutate
13
+ from .oracle import Oracle
14
+
15
+ from ...base import BaseMolecularGenerator
16
+
17
+ @dataclass
18
+ class GraphGAMolecularGenerator(BaseMolecularGenerator):
19
+ """This generator implements the Graph Genetic Algorithm for molecular generation.
20
+
21
+ References
22
+ ----------
23
+ - A Graph-Based Genetic Algorithm and Its Application to the Multiobjective Evolution of
24
+ Median Molecules. Journal of Chemical Information and Computer Sciences. https://pubs.acs.org/doi/10.1021/ci034290p
25
+ - Implementation: https://github.com/wenhao-gao/mol_opt
26
+
27
+ Parameters
28
+ ----------
29
+ num_task : int, default=0
30
+ Number of properties to condition on. Set to 0 for unconditional generation.
31
+ population_size : int, default=100
32
+ Size of the population in each iteration.
33
+ offspring_size : int, default=50
34
+ Number of offspring molecules to generate in each iteration.
35
+ mutation_rate : float, default=0.0067
36
+ Probability of mutation occurring during reproduction.
37
+ n_jobs : int, default=1
38
+ Number of parallel jobs to run. -1 means using all processors.
39
+ iteration : int, default=5
40
+ Number of iterations for each target label (or random sample) to run the genetic algorithm.
41
+ verbose : bool, default=False
42
+ Whether to display progress bars and logs.
43
+ """
44
+
45
+ # GA parameters
46
+ num_task: int = 0
47
+ population_size: int = 100
48
+ offspring_size: int = 50
49
+ mutation_rate: float = 0.0067
50
+ n_jobs: int = 1
51
+ iteration: int = 5
52
+
53
+ # Other parameters
54
+ verbose: bool = False
55
+ model_name: str = "GraphGAMolecularGenerator"
56
+ model_class = None
57
+
58
+ def __post_init__(self):
59
+ super().__post_init__()
60
+
61
+ @staticmethod
62
+ def _get_param_names() -> List[str]:
63
+ return [
64
+ "num_task", "population_size", "offspring_size", "mutation_rate",
65
+ "n_jobs", "iteration", "verbose"
66
+ ]
67
+
68
+ def _get_model_params(self, checkpoint: Optional[Dict] = None) -> Dict[str, Any]:
69
+ raise NotImplementedError("GraphGA does not support getting model parameters")
70
+
71
+ def save_to_local(self, path: str):
72
+ joblib.dump(self.oracle, path)
73
+ if self.verbose:
74
+ print(f"Saved oracle to {path}")
75
+
76
+ def load_from_local(self):
77
+ raise NotImplementedError(
78
+ "GraphGA does not support loading from local. "
79
+ "If you want to load the oracles saved through save_to_local, "
80
+ "you need to manually load the oracle from the path with joblib.load(path) "
81
+ "and pass it to the fit function."
82
+ )
83
+
84
+ def save_to_hf(self, repo_id: str, task_id: str = "default"):
85
+ raise NotImplementedError("GraphGA does not support pushing to huggingface")
86
+
87
+ def load_from_hf(self, repo_id: str, task_id: str = "default"):
88
+ raise NotImplementedError("GraphGA does not support loading from huggingface")
89
+
90
+ def _setup_optimizers(self):
91
+ raise NotImplementedError("GraphGA does not support setting up optimizers")
92
+
93
+ def _train_epoch(self, train_loader, optimizer):
94
+ raise NotImplementedError("GraphGA does not support training epochs")
95
+
96
+ def fit(
97
+ self,
98
+ X_train: List[str],
99
+ y_train: Optional[Union[List, np.ndarray]] = None,
100
+ oracle: Optional[List[Callable]] = None
101
+ ) -> "GraphGAMolecularGenerator":
102
+ """Fit the model to the training data.
103
+
104
+ Parameters
105
+ ----------
106
+ X_train : List[str]
107
+ Training data, which will be used as the initial population.
108
+ y_train : Optional[Union[List, np.ndarray]]
109
+ Training labels for conditional generation (num_task is not 0).
110
+ oracle : Optional[Callable]
111
+ Oracle used to score the generated molecules. If not provided, default oracles based on
112
+ ``sklearn.ensemble.RandomForestRegressor`` are trained on the X_train and y_train.
113
+
114
+ For a customized oracle, it should be a Callable object, i.e., ``oracle(X, y)``.
115
+ Please properly wrap your oracle to take two inputs:
116
+ - a list of ``rdkit.Chem.rdchem.Mol`` objects and
117
+ - a (1, num_task) numpy array of target values that all the molecules in the list target to achieve. Take care of NaN values if any.
118
+
119
+ Scores for different tasks should be aggregated, i.e., mean or sum. The return should be a list of scores (float).
120
+ Smaller scores mean closer to the target goal.
121
+
122
+ Oracles are not needed for unconditional generation.
123
+
124
+ Returns
125
+ -------
126
+ self : GraphGAMolecularGenerator
127
+ Fitted model.
128
+ """
129
+ self.y_train = None
130
+ if oracle is not None:
131
+ self.oracle = oracle
132
+ else:
133
+ X_train, y_train = self._validate_inputs(X_train, y_train, num_task=self.num_task, return_rdkit_mol=False)
134
+ if y_train is not None:
135
+ warn("No oracles provided but y_train is provided, using default oracles (RandomForestRegressor)", UserWarning)
136
+ self.oracle = Oracle(num_task=self.num_task)
137
+ self.oracle.fit(X_train, y_train)
138
+ self.y_train = y_train
139
+ else:
140
+ assert self.num_task == 0, "No oracles or y_train provided but num_task is not 0"
141
+ self.oracle = None
142
+
143
+ self.X_train = X_train
144
+ self.is_fitted_ = True
145
+ return self
146
+
147
+ def _make_mating_pool(self, population_mol, population_scores, offspring_size: int):
148
+ """Create mating pool where smaller scores have higher selection probabilities."""
149
+ max_score = max(population_scores)
150
+ # Invert scores so that smaller scores become larger probabilities
151
+ inverted_scores = [max_score - s + 1e-6 for s in population_scores] # Add small constant to avoid zeros
152
+ sum_scores = sum(inverted_scores)
153
+ population_probs = [p / sum_scores for p in inverted_scores]
154
+ mating_pool = np.random.choice(population_mol, p=population_probs, size=offspring_size, replace=True)
155
+ return mating_pool
156
+
157
+ def _reproduce(self, mating_pool, mutation_rate):
158
+ """Create new molecule through crossover and mutation."""
159
+ parent_a = random.choice(mating_pool)
160
+ parent_b = random.choice(mating_pool)
161
+ new_child = crossover(parent_a, parent_b)
162
+ if new_child is not None:
163
+ new_child = mutate(new_child, mutation_rate)
164
+ return new_child
165
+
166
+ def _sanitize_molecules(self, population_mol):
167
+ """Sanitize molecules by removing duplicates and invalid molecules."""
168
+ new_mol_list = []
169
+ smiles_set = set()
170
+ for mol in population_mol:
171
+ if mol is not None:
172
+ try:
173
+ smiles = Chem.MolToSmiles(mol)
174
+ if smiles is not None and smiles not in smiles_set:
175
+ smiles_set.add(smiles)
176
+ new_mol_list.append(mol)
177
+ except ValueError:
178
+ pass
179
+ return new_mol_list
180
+
181
+ def _get_score(self, mol_list, label):
182
+ if label is None:
183
+ return [1.0] * len(mol_list) # For unconditional generation
184
+ return self.oracle(mol_list, label)
185
+
186
+ def generate(
187
+ self,
188
+ labels: Optional[Union[List[List], np.ndarray]] = None,
189
+ num_samples: int = 32
190
+ ) -> List[str]:
191
+ """Generate molecules using genetic algorithm optimization."""
192
+ if not self.is_fitted_:
193
+ raise RuntimeError("Model must be fitted before generating")
194
+
195
+ all_generated_mols = []
196
+
197
+ if labels is not None:
198
+ try:
199
+ labels = np.array(labels).reshape(-1, self.num_task)
200
+ except:
201
+ raise ValueError(f"labels must be convertible to a numpy array with shape (-1, {self.num_task})")
202
+
203
+ # Prepare all inputs for parallel processing
204
+ parallel_inputs = []
205
+ for i in range(labels.shape[0]):
206
+ label = labels[i:i+1] # Keep as 2D array
207
+
208
+ # Initialize population based on similarity to target label
209
+ if self.y_train is not None:
210
+ population_mol = self._initialize_population_for_label(label)
211
+ else:
212
+ population_idx = np.random.choice(len(self.X_train), min(self.population_size, len(self.X_train)))
213
+ population_smiles = [self.X_train[idx] for idx in population_idx]
214
+ population_mol = [Chem.MolFromSmiles(s) for s in population_smiles]
215
+
216
+ parallel_inputs.append((population_mol, label))
217
+
218
+ # Run GA for all labels in parallel with tqdm progress bar
219
+ if self.verbose:
220
+ results = joblib.Parallel(n_jobs=self.n_jobs)(
221
+ delayed(self._run_generation)(pop_mol, lbl)
222
+ for pop_mol, lbl in tqdm(parallel_inputs, desc="Generating molecules")
223
+ )
224
+ else:
225
+ results = joblib.Parallel(n_jobs=self.n_jobs)(
226
+ delayed(self._run_generation)(pop_mol, lbl)
227
+ for pop_mol, lbl in parallel_inputs
228
+ )
229
+
230
+ # Convert results to SMILES in the original order
231
+ all_generated_mols = [Chem.MolToSmiles(mol) for mol in results]
232
+ else:
233
+ # Prepare all inputs for parallel processing
234
+ parallel_inputs = []
235
+ for _ in range(num_samples):
236
+ population_idx = np.random.choice(len(self.X_train), min(self.population_size, len(self.X_train)))
237
+ population_smiles = [self.X_train[idx] for idx in population_idx]
238
+ population_mol = [Chem.MolFromSmiles(s) for s in population_smiles]
239
+ parallel_inputs.append((population_mol, None))
240
+
241
+ # Run GA for all samples in parallel with tqdm progress bar
242
+ if self.verbose:
243
+ results = joblib.Parallel(n_jobs=self.n_jobs)(
244
+ delayed(self._run_generation)(pop_mol, lbl)
245
+ for pop_mol, lbl in tqdm(parallel_inputs, desc="Generating molecules", total=num_samples)
246
+ )
247
+ else:
248
+ results = joblib.Parallel(n_jobs=self.n_jobs)(
249
+ delayed(self._run_generation)(pop_mol, lbl)
250
+ for pop_mol, lbl in parallel_inputs
251
+ )
252
+
253
+ # Convert results to SMILES
254
+ all_generated_mols = [Chem.MolToSmiles(mol) for mol in results]
255
+
256
+ return all_generated_mols
257
+
258
+ def _initialize_population_for_label(self, label):
259
+ """Initialize population based on similarity to target label."""
260
+ similarities = []
261
+
262
+ for i in range(len(self.X_train)):
263
+ sample_label = self.y_train[i]
264
+ similarity = -np.nansum((sample_label - label[0])**2)
265
+ similarities.append((i, similarity))
266
+
267
+ if similarities:
268
+ similarities.sort(key=lambda x: x[1], reverse=True)
269
+ top_indices = [x[0] for x in similarities[:self.population_size]]
270
+ else:
271
+ top_indices = np.random.choice(len(self.X_train), min(self.population_size, len(self.X_train)))
272
+
273
+ population_smiles = [self.X_train[i] for i in top_indices]
274
+ return [Chem.MolFromSmiles(s) for s in population_smiles]
275
+
276
+ def _run_generation(self, population_mol, label):
277
+ """Run the genetic algorithm for a specific population and label."""
278
+ for generation_idx in range(self.iteration):
279
+ if label is not None:
280
+ population_scores = self._get_score(population_mol, label)
281
+ else:
282
+ population_scores = [1.0] * len(population_mol) # For unconditional generation
283
+
284
+ mating_pool = self._make_mating_pool(population_mol, population_scores, self.offspring_size)
285
+
286
+ # Create offspring sequentially (parallelization is at the higher level now)
287
+ offspring_mol = []
288
+ for _ in range(self.offspring_size):
289
+ offspring = self._reproduce(mating_pool, self.mutation_rate)
290
+ offspring_mol.append(offspring)
291
+
292
+ population_mol += offspring_mol
293
+ population_mol = self._sanitize_molecules(population_mol)
294
+
295
+ # Re-score the expanded population
296
+ if label is not None:
297
+ population_scores = self._get_score(population_mol, label)
298
+ else:
299
+ population_scores = [1.0] * len(population_mol)
300
+
301
+ # Select top molecules for next generation
302
+ population_tuples = list(zip(population_scores, population_mol))
303
+ population_tuples = sorted(population_tuples, key=lambda x: x[0], reverse=False) # lower score is better
304
+ population_tuples = population_tuples[:self.population_size]
305
+
306
+ population_mol = [t[1] for t in population_tuples]
307
+
308
+ # Return the best molecule
309
+ return population_mol[0]
310
+
@@ -0,0 +1,136 @@
1
+ import random
2
+
3
+ import numpy as np
4
+ from rdkit import Chem, rdBase
5
+ from rdkit.Chem import AllChem
6
+
7
+ from .crossover import mol_ok, ring_OK
8
+
9
+ rdBase.DisableLog('rdApp.error')
10
+
11
+ def delete_atom():
12
+ choices = ['[*:1]~[D1:2]>>[*:1]', '[*:1]~[D2:2]~[*:3]>>[*:1]-[*:3]',
13
+ '[*:1]~[D3:2](~[*;!H0:3])~[*:4]>>[*:1]-[*:3]-[*:4]',
14
+ '[*:1]~[D4:2](~[*;!H0:3])(~[*;!H0:4])~[*:5]>>[*:1]-[*:3]-[*:4]-[*:5]',
15
+ '[*:1]~[D4:2](~[*;!H0;!H1:3])(~[*:4])~[*:5]>>[*:1]-[*:3](-[*:4])-[*:5]']
16
+ p = [0.25, 0.25, 0.25, 0.1875, 0.0625]
17
+
18
+ return np.random.choice(choices, p=p)
19
+
20
+
21
+ def append_atom():
22
+ choices = [['single', ['C', 'N', 'O', 'F', 'S', 'Cl', 'Br'], 7 * [1.0 / 7.0]],
23
+ ['double', ['C', 'N', 'O'], 3 * [1.0 / 3.0]],
24
+ ['triple', ['C', 'N'], 2 * [1.0 / 2.0]]]
25
+ p_BO = [0.60, 0.35, 0.05]
26
+
27
+ index = np.random.choice(list(range(3)), p=p_BO)
28
+
29
+ BO, atom_list, p = choices[index]
30
+ new_atom = np.random.choice(atom_list, p=p)
31
+
32
+ if BO == 'single':
33
+ rxn_smarts = '[*;!H0:1]>>[*:1]X'.replace('X', '-' + new_atom)
34
+ if BO == 'double':
35
+ rxn_smarts = '[*;!H0;!H1:1]>>[*:1]X'.replace('X', '=' + new_atom)
36
+ if BO == 'triple':
37
+ rxn_smarts = '[*;H3:1]>>[*:1]X'.replace('X', '#' + new_atom)
38
+
39
+ return rxn_smarts
40
+
41
+
42
+ def insert_atom():
43
+ choices = [['single', ['C', 'N', 'O', 'S'], 4 * [1.0 / 4.0]],
44
+ ['double', ['C', 'N'], 2 * [1.0 / 2.0]],
45
+ ['triple', ['C'], [1.0]]]
46
+ p_BO = [0.60, 0.35, 0.05]
47
+
48
+ index = np.random.choice(list(range(3)), p=p_BO)
49
+
50
+ BO, atom_list, p = choices[index]
51
+ new_atom = np.random.choice(atom_list, p=p)
52
+
53
+ if BO == 'single':
54
+ rxn_smarts = '[*:1]~[*:2]>>[*:1]X[*:2]'.replace('X', new_atom)
55
+ if BO == 'double':
56
+ rxn_smarts = '[*;!H0:1]~[*:2]>>[*:1]=X-[*:2]'.replace('X', new_atom)
57
+ if BO == 'triple':
58
+ rxn_smarts = '[*;!R;!H1;!H0:1]~[*:2]>>[*:1]#X-[*:2]'.replace('X', new_atom)
59
+
60
+ return rxn_smarts
61
+
62
+
63
+ def change_bond_order():
64
+ choices = ['[*:1]!-[*:2]>>[*:1]-[*:2]', '[*;!H0:1]-[*;!H0:2]>>[*:1]=[*:2]',
65
+ '[*:1]#[*:2]>>[*:1]=[*:2]', '[*;!R;!H1;!H0:1]~[*:2]>>[*:1]#[*:2]']
66
+ p = [0.45, 0.45, 0.05, 0.05]
67
+
68
+ return np.random.choice(choices, p=p)
69
+
70
+
71
+ def delete_cyclic_bond():
72
+ return '[*:1]@[*:2]>>([*:1].[*:2])'
73
+
74
+
75
+ def add_ring():
76
+ choices = ['[*;!r;!H0:1]~[*;!r:2]~[*;!r;!H0:3]>>[*:1]1~[*:2]~[*:3]1',
77
+ '[*;!r;!H0:1]~[*!r:2]~[*!r:3]~[*;!r;!H0:4]>>[*:1]1~[*:2]~[*:3]~[*:4]1',
78
+ '[*;!r;!H0:1]~[*!r:2]~[*:3]~[*:4]~[*;!r;!H0:5]>>[*:1]1~[*:2]~[*:3]~[*:4]~[*:5]1',
79
+ '[*;!r;!H0:1]~[*!r:2]~[*:3]~[*:4]~[*!r:5]~[*;!r;!H0:6]>>[*:1]1~[*:2]~[*:3]~[*:4]~[*:5]~[*:6]1']
80
+ p = [0.05, 0.05, 0.45, 0.45]
81
+
82
+ return np.random.choice(choices, p=p)
83
+
84
+
85
+ def change_atom(mol):
86
+ choices = ['#6', '#7', '#8', '#9', '#16', '#17', '#35']
87
+ p = [0.15, 0.15, 0.14, 0.14, 0.14, 0.14, 0.14]
88
+
89
+ X = np.random.choice(choices, p=p)
90
+ while not mol.HasSubstructMatch(Chem.MolFromSmarts('[' + X + ']')):
91
+ X = np.random.choice(choices, p=p)
92
+ Y = np.random.choice(choices, p=p)
93
+ while Y == X:
94
+ Y = np.random.choice(choices, p=p)
95
+
96
+ return '[X:1]>>[Y:1]'.replace('X', X).replace('Y', Y)
97
+
98
+
99
+ def mutate(mol, mutation_rate):
100
+ if random.random() > mutation_rate:
101
+ return mol
102
+
103
+ try:
104
+ Chem.Kekulize(mol, clearAromaticFlags=True)
105
+ except ValueError:
106
+ return mol
107
+
108
+ p = [0.15, 0.14, 0.14, 0.14, 0.14, 0.14, 0.15]
109
+ for i in range(10):
110
+ rxn_smarts_list = 7 * ['']
111
+ rxn_smarts_list[0] = insert_atom()
112
+ rxn_smarts_list[1] = change_bond_order()
113
+ rxn_smarts_list[2] = delete_cyclic_bond()
114
+ rxn_smarts_list[3] = add_ring()
115
+ rxn_smarts_list[4] = delete_atom()
116
+ rxn_smarts_list[5] = change_atom(mol)
117
+ rxn_smarts_list[6] = append_atom()
118
+ rxn_smarts = np.random.choice(rxn_smarts_list, p=p)
119
+
120
+ # print 'mutation',rxn_smarts
121
+
122
+ rxn = AllChem.ReactionFromSmarts(rxn_smarts)
123
+
124
+ new_mol_trial = rxn.RunReactants((mol,))
125
+
126
+ new_mols = []
127
+ for m in new_mol_trial:
128
+ m = m[0]
129
+ # print Chem.MolToSmiles(mol),mol_ok(mol)
130
+ if mol_ok(m) and ring_OK(m):
131
+ new_mols.append(m)
132
+
133
+ if len(new_mols) > 0:
134
+ return random.choice(new_mols)
135
+
136
+ return None
@@ -0,0 +1,95 @@
1
+ import numpy as np
2
+ from sklearn.ensemble import RandomForestRegressor
3
+ from rdkit import Chem
4
+ from typing import List, Any
5
+ from ...utils.graph.features import getmorganfingerprint
6
+
7
+
8
+ class Oracle:
9
+ """The default Oracle class for scoring molecules in GraphGA.
10
+
11
+ This class wraps predictive models (like RandomForestRegressor) to score molecules
12
+ based on their properties. It handles conversion of SMILES to fingerprints.
13
+
14
+ Parameters
15
+ ----------
16
+ models : List[Any], optional
17
+ List of trained models that implement a predict method.
18
+ If None, RandomForestRegressors will be created when fit is called.
19
+ num_task : int, default=1
20
+ Number of properties to predict.
21
+ """
22
+
23
+ def __init__(self, models=None, num_task=1):
24
+ self.models = models if models is not None else [RandomForestRegressor() for _ in range(num_task)]
25
+ self.num_task = num_task if models is None else len(models)
26
+
27
+ def _convert_to_fingerprint(self, molecules):
28
+ """Convert SMILES or RDKit molecules to fingerprints."""
29
+ if isinstance(molecules[0], str):
30
+ return np.array([getmorganfingerprint(Chem.MolFromSmiles(mol)) for mol in molecules])
31
+ else:
32
+ return np.array([getmorganfingerprint(mol) for mol in molecules])
33
+
34
+ def fit(self, X_train, y_train):
35
+ """Fit the underlying models with training data.
36
+
37
+ Parameters
38
+ ----------
39
+ X_train : List[str] or List[RDKit.Mol]
40
+ Training molecules as SMILES strings or RDKit Mol objects.
41
+ y_train : np.ndarray
42
+ Training labels with shape (n_samples, num_task).
43
+
44
+ Returns
45
+ -------
46
+ self : Oracle
47
+ Fitted oracle.
48
+ """
49
+ X_train_fp = self._convert_to_fingerprint(X_train)
50
+
51
+ for i in range(self.num_task):
52
+ nan_mask = ~np.isnan(y_train[:, i])
53
+ y_train_ = y_train[:, i][nan_mask]
54
+ X_train_fp_ = X_train_fp[nan_mask]
55
+ self.models[i].fit(X_train_fp_, y_train_)
56
+
57
+ return self
58
+
59
+ def __call__(self, molecules, target_values):
60
+ """Score molecules based on their predicted properties.
61
+
62
+ Parameters
63
+ ----------
64
+ molecules : List[str] or List[RDKit.Mol]
65
+ Molecules to score as SMILES strings or RDKit Mol objects.
66
+ target_values : np.ndarray,
67
+ Scores will be based on distance to these targets.
68
+
69
+ Returns
70
+ -------
71
+ List[float]
72
+ Scores for each molecule.
73
+ """
74
+ fps = self._convert_to_fingerprint(molecules)
75
+ scores_list = []
76
+
77
+ for i, fp in enumerate(fps):
78
+ if self.num_task == 1:
79
+ score = self.models[0].predict([fp])[0]
80
+ scores_list.append(float(score))
81
+ else:
82
+ mol_scores = []
83
+ for idx in range(self.num_task):
84
+ pred = self.models[idx].predict([fp])[0]
85
+
86
+ if target_values is not None and not np.isnan(target_values[0][idx]):
87
+ # Lower score for values closer to target
88
+ target = target_values[0][idx]
89
+ dist = abs(float(pred) - target) / (abs(target) + 1e-8)
90
+ mol_scores.append(dist)
91
+
92
+ score = np.nanmean(mol_scores) # Lower is better when using distances
93
+ scores_list.append(float(score))
94
+
95
+ return scores_list
@@ -0,0 +1,3 @@
1
+ from .modeling_jtvae import JTVAEMolecularGenerator
2
+
3
+ __all__ = ['JTVAEMolecularGenerator']