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.
- torch_molecule/__init__.py +70 -0
- torch_molecule/base/__init__.py +8 -0
- torch_molecule/base/base.py +396 -0
- torch_molecule/base/encoder.py +44 -0
- torch_molecule/base/generator.py +24 -0
- torch_molecule/base/old_encoder.py +322 -0
- torch_molecule/base/old_predictor.py +904 -0
- torch_molecule/base/old_predictor2.py +436 -0
- torch_molecule/base/predictor.py +112 -0
- torch_molecule/encoder/attrmask/__init__.py +3 -0
- torch_molecule/encoder/attrmask/model.py +113 -0
- torch_molecule/encoder/attrmask/modeling_attrmask.py +304 -0
- torch_molecule/encoder/constant.py +13 -0
- torch_molecule/encoder/contextpred/__init__.py +3 -0
- torch_molecule/encoder/contextpred/model.py +207 -0
- torch_molecule/encoder/contextpred/modeling_contextpred.py +299 -0
- torch_molecule/encoder/contextpred/utils.py +206 -0
- torch_molecule/encoder/edgepred/__init__.py +3 -0
- torch_molecule/encoder/edgepred/model.py +104 -0
- torch_molecule/encoder/edgepred/modeling_edgepred.py +285 -0
- torch_molecule/encoder/graphmae/__init__.py +3 -0
- torch_molecule/encoder/graphmae/dataloader.py +206 -0
- torch_molecule/encoder/graphmae/model.py +137 -0
- torch_molecule/encoder/graphmae/modeling_graphmae.py +323 -0
- torch_molecule/encoder/infograph/__init__.py +3 -0
- torch_molecule/encoder/infograph/loss_function.py +128 -0
- torch_molecule/encoder/infograph/model.py +92 -0
- torch_molecule/encoder/infograph/modeling_infograph.py +301 -0
- torch_molecule/encoder/infograph/module.py +31 -0
- torch_molecule/encoder/moama/__init__.py +3 -0
- torch_molecule/encoder/moama/model.py +131 -0
- torch_molecule/encoder/moama/modeling_moama.py +307 -0
- torch_molecule/encoder/moama/utils.py +143 -0
- torch_molecule/encoder/pretrained/__init__.py +3 -0
- torch_molecule/encoder/pretrained/modeling_pretrained.py +274 -0
- torch_molecule/encoder/supervised/__init__.py +3 -0
- torch_molecule/encoder/supervised/model.py +97 -0
- torch_molecule/encoder/supervised/modeling_supervised.py +354 -0
- torch_molecule/generator/digress/__init__.py +3 -0
- torch_molecule/generator/digress/diffusion.py +194 -0
- torch_molecule/generator/digress/modeling_digress.py +503 -0
- torch_molecule/generator/digress/transformer.py +360 -0
- torch_molecule/generator/digress/utils.py +196 -0
- torch_molecule/generator/gdss/__init__.py +3 -0
- torch_molecule/generator/gdss/layers.py +257 -0
- torch_molecule/generator/gdss/model.py +204 -0
- torch_molecule/generator/gdss/modeling_gdss.py +474 -0
- torch_molecule/generator/gdss/sde.py +334 -0
- torch_molecule/generator/gdss/solver.py +227 -0
- torch_molecule/generator/gdss/toremove/ScoreNetworkA.py +167 -0
- torch_molecule/generator/gdss/toremove/ScoreNetworkX.py +85 -0
- torch_molecule/generator/gdss/utils.py +240 -0
- torch_molecule/generator/graph_dit/__init__.py +3 -0
- torch_molecule/generator/graph_dit/diffusion.py +188 -0
- torch_molecule/generator/graph_dit/modeling_graph_dit.py +581 -0
- torch_molecule/generator/graph_dit/transformer.py +207 -0
- torch_molecule/generator/graph_dit/utils.py +220 -0
- torch_molecule/generator/graph_ga/__init__.py +3 -0
- torch_molecule/generator/graph_ga/crossover.py +182 -0
- torch_molecule/generator/graph_ga/modeling_graph_ga.py +310 -0
- torch_molecule/generator/graph_ga/mutate.py +136 -0
- torch_molecule/generator/graph_ga/oracle.py +95 -0
- torch_molecule/generator/jtvae/__init__.py +3 -0
- torch_molecule/generator/jtvae/jtnn/chemutils.py +450 -0
- torch_molecule/generator/jtvae/jtnn/datautils.py +127 -0
- torch_molecule/generator/jtvae/jtnn/jtmpn.py +140 -0
- torch_molecule/generator/jtvae/jtnn/jtnn_dec.py +350 -0
- torch_molecule/generator/jtvae/jtnn/jtnn_enc.py +131 -0
- torch_molecule/generator/jtvae/jtnn/mol_tree.py +111 -0
- torch_molecule/generator/jtvae/jtnn/mpn.py +129 -0
- torch_molecule/generator/jtvae/jtnn/nnutils.py +66 -0
- torch_molecule/generator/jtvae/jtnn/vocab.py +31 -0
- torch_molecule/generator/jtvae/jtnn_vae.py +246 -0
- torch_molecule/generator/jtvae/modeling_jtvae.py +247 -0
- torch_molecule/generator/lstm/__init__.py +3 -0
- torch_molecule/generator/lstm/action_sampler.py +91 -0
- torch_molecule/generator/lstm/lstm.py +59 -0
- torch_molecule/generator/lstm/modeling_lstm.py +263 -0
- torch_molecule/generator/lstm/smiles_char_dict.py +123 -0
- torch_molecule/generator/lstm/utils.py +62 -0
- torch_molecule/generator/molgpt/__init__.py +3 -0
- torch_molecule/generator/molgpt/dataset.py +87 -0
- torch_molecule/generator/molgpt/gpt.py +251 -0
- torch_molecule/generator/molgpt/modeling_molgpt.py +427 -0
- torch_molecule/generator/pretrained/modeling_pretrained.py +1 -0
- torch_molecule/nn/__init__.py +16 -0
- torch_molecule/nn/attention.py +63 -0
- torch_molecule/nn/embedder.py +183 -0
- torch_molecule/nn/gnn.py +631 -0
- torch_molecule/nn/mlp.py +34 -0
- torch_molecule/predictor/bfgnn/__init__.py +3 -0
- torch_molecule/predictor/bfgnn/model.py +116 -0
- torch_molecule/predictor/bfgnn/modeling_bfgnn.py +81 -0
- torch_molecule/predictor/dir/__init__.py +3 -0
- torch_molecule/predictor/dir/model.py +235 -0
- torch_molecule/predictor/dir/modeling_dir.py +161 -0
- torch_molecule/predictor/dir/utils.py +40 -0
- torch_molecule/predictor/gnn/__init__.py +3 -0
- torch_molecule/predictor/gnn/model.py +114 -0
- torch_molecule/predictor/gnn/modeling_gnn.py +706 -0
- torch_molecule/predictor/gnn/old_modeling_gnn.py +707 -0
- torch_molecule/predictor/grea/__init__.py +3 -0
- torch_molecule/predictor/grea/model.py +190 -0
- torch_molecule/predictor/grea/modeling_grea.py +107 -0
- torch_molecule/predictor/grea/old1_features.py +229 -0
- torch_molecule/predictor/grea/old1_modeling_grea.py +493 -0
- torch_molecule/predictor/grea/utils.py +97 -0
- torch_molecule/predictor/grin/__init__.py +3 -0
- torch_molecule/predictor/grin/model.py +116 -0
- torch_molecule/predictor/grin/modeling_grin.py +91 -0
- torch_molecule/predictor/irm/__init__.py +3 -0
- torch_molecule/predictor/irm/model.py +127 -0
- torch_molecule/predictor/irm/modeling_irm.py +179 -0
- torch_molecule/predictor/lstm/__init__.py +3 -0
- torch_molecule/predictor/lstm/model.py +145 -0
- torch_molecule/predictor/lstm/modeling_lstm.py +627 -0
- torch_molecule/predictor/lstm/token_from_smiles.py +85 -0
- torch_molecule/predictor/old_modeling_graph.py +650 -0
- torch_molecule/predictor/rpgnn/__init__.py +3 -0
- torch_molecule/predictor/rpgnn/model.py +185 -0
- torch_molecule/predictor/rpgnn/modeling_rpgnn.py +76 -0
- torch_molecule/predictor/sgir/__init__.py +3 -0
- torch_molecule/predictor/sgir/modeling_sgir.py +260 -0
- torch_molecule/predictor/sgir/strategy.py +266 -0
- torch_molecule/predictor/smiles_transformer/__init__.py +3 -0
- torch_molecule/predictor/smiles_transformer/model.py +424 -0
- torch_molecule/predictor/smiles_transformer/modeling_transformer.py +252 -0
- torch_molecule/predictor/ssr/__init__.py +3 -0
- torch_molecule/predictor/ssr/model.py +283 -0
- torch_molecule/predictor/ssr/modeling_ssr.py +267 -0
- torch_molecule/predictor/ssr/utils.py +185 -0
- torch_molecule/utils/__init__.py +31 -0
- torch_molecule/utils/checker.py +158 -0
- torch_molecule/utils/checkpoint.py +294 -0
- torch_molecule/utils/format.py +140 -0
- torch_molecule/utils/generic/metrics.py +530 -0
- torch_molecule/utils/generic/pseudo_tasks.py +11 -0
- torch_molecule/utils/generic/weights.py +30 -0
- torch_molecule/utils/graph/features.py +178 -0
- torch_molecule/utils/graph/graph_from_smiles.py +134 -0
- torch_molecule/utils/graph/graph_to_smiles.py +312 -0
- torch_molecule/utils/hf.py +232 -0
- torch_molecule/utils/search.py +64 -0
- torch_molecule-0.1.0.dev2.dist-info/METADATA +243 -0
- torch_molecule-0.1.0.dev2.dist-info/RECORD +148 -0
- torch_molecule-0.1.0.dev2.dist-info/WHEEL +5 -0
- torch_molecule-0.1.0.dev2.dist-info/licenses/LICENSE +21 -0
- 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
|