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,111 @@
|
|
|
1
|
+
import rdkit
|
|
2
|
+
import rdkit.Chem as Chem
|
|
3
|
+
from .chemutils import get_clique_mol, tree_decomp, get_mol, get_smiles, set_atommap, enum_assemble, decode_stereo
|
|
4
|
+
from .vocab import *
|
|
5
|
+
|
|
6
|
+
class MolTreeNode(object):
|
|
7
|
+
|
|
8
|
+
def __init__(self, smiles, clique=[]):
|
|
9
|
+
self.smiles = smiles
|
|
10
|
+
self.mol = get_mol(self.smiles)
|
|
11
|
+
|
|
12
|
+
self.clique = [x for x in clique] #copy
|
|
13
|
+
self.neighbors = []
|
|
14
|
+
|
|
15
|
+
def add_neighbor(self, nei_node):
|
|
16
|
+
self.neighbors.append(nei_node)
|
|
17
|
+
|
|
18
|
+
def recover(self, original_mol):
|
|
19
|
+
clique = []
|
|
20
|
+
clique.extend(self.clique)
|
|
21
|
+
if not self.is_leaf:
|
|
22
|
+
for cidx in self.clique:
|
|
23
|
+
original_mol.GetAtomWithIdx(cidx).SetAtomMapNum(self.nid)
|
|
24
|
+
|
|
25
|
+
for nei_node in self.neighbors:
|
|
26
|
+
clique.extend(nei_node.clique)
|
|
27
|
+
if nei_node.is_leaf: #Leaf node, no need to mark
|
|
28
|
+
continue
|
|
29
|
+
for cidx in nei_node.clique:
|
|
30
|
+
#allow singleton node override the atom mapping
|
|
31
|
+
if cidx not in self.clique or len(nei_node.clique) == 1:
|
|
32
|
+
atom = original_mol.GetAtomWithIdx(cidx)
|
|
33
|
+
atom.SetAtomMapNum(nei_node.nid)
|
|
34
|
+
|
|
35
|
+
clique = list(set(clique))
|
|
36
|
+
label_mol = get_clique_mol(original_mol, clique)
|
|
37
|
+
self.label = Chem.MolToSmiles(Chem.MolFromSmiles(get_smiles(label_mol)))
|
|
38
|
+
|
|
39
|
+
for cidx in clique:
|
|
40
|
+
original_mol.GetAtomWithIdx(cidx).SetAtomMapNum(0)
|
|
41
|
+
|
|
42
|
+
return self.label
|
|
43
|
+
|
|
44
|
+
def assemble(self):
|
|
45
|
+
neighbors = [nei for nei in self.neighbors if nei.mol.GetNumAtoms() > 1]
|
|
46
|
+
neighbors = sorted(neighbors, key=lambda x:x.mol.GetNumAtoms(), reverse=True)
|
|
47
|
+
singletons = [nei for nei in self.neighbors if nei.mol.GetNumAtoms() == 1]
|
|
48
|
+
neighbors = singletons + neighbors
|
|
49
|
+
|
|
50
|
+
cands,aroma = enum_assemble(self, neighbors)
|
|
51
|
+
new_cands = [cand for i,cand in enumerate(cands) if aroma[i] >= 0]
|
|
52
|
+
if len(new_cands) > 0: cands = new_cands
|
|
53
|
+
|
|
54
|
+
if len(cands) > 0:
|
|
55
|
+
self.cands, _ = zip(*cands)
|
|
56
|
+
self.cands = list(self.cands)
|
|
57
|
+
else:
|
|
58
|
+
self.cands = []
|
|
59
|
+
|
|
60
|
+
class MolTree(object):
|
|
61
|
+
|
|
62
|
+
def __init__(self, smiles, voc=None):
|
|
63
|
+
self.smiles = smiles
|
|
64
|
+
self.mol = get_mol(smiles)
|
|
65
|
+
self.voc = voc
|
|
66
|
+
|
|
67
|
+
#Stereo Generation (currently disabled)
|
|
68
|
+
#mol = Chem.MolFromSmiles(smiles)
|
|
69
|
+
#self.smiles3D = Chem.MolToSmiles(mol, isomericSmiles=True)
|
|
70
|
+
#self.smiles2D = Chem.MolToSmiles(mol)
|
|
71
|
+
#self.stereo_cands = decode_stereo(self.smiles2D)
|
|
72
|
+
|
|
73
|
+
cliques, edges = tree_decomp(self.mol)
|
|
74
|
+
self.nodes = []
|
|
75
|
+
root = 0
|
|
76
|
+
for i,c in enumerate(cliques):
|
|
77
|
+
cmol = get_clique_mol(self.mol, c)
|
|
78
|
+
node = MolTreeNode(get_smiles(cmol), c)
|
|
79
|
+
self.nodes.append(node)
|
|
80
|
+
if min(c) == 0: root = i
|
|
81
|
+
|
|
82
|
+
for x,y in edges:
|
|
83
|
+
self.nodes[x].add_neighbor(self.nodes[y])
|
|
84
|
+
self.nodes[y].add_neighbor(self.nodes[x])
|
|
85
|
+
|
|
86
|
+
if root > 0:
|
|
87
|
+
self.nodes[0],self.nodes[root] = self.nodes[root],self.nodes[0]
|
|
88
|
+
|
|
89
|
+
for i,node in enumerate(self.nodes):
|
|
90
|
+
node.nid = i + 1
|
|
91
|
+
if len(node.neighbors) > 1: #Leaf node mol is not marked
|
|
92
|
+
set_atommap(node.mol, node.nid)
|
|
93
|
+
node.is_leaf = (len(node.neighbors) == 1)
|
|
94
|
+
|
|
95
|
+
def size(self):
|
|
96
|
+
return len(self.nodes)
|
|
97
|
+
|
|
98
|
+
def recover(self):
|
|
99
|
+
for node in self.nodes:
|
|
100
|
+
node.recover(self.mol)
|
|
101
|
+
|
|
102
|
+
def assemble(self):
|
|
103
|
+
for node in self.nodes:
|
|
104
|
+
node.assemble()
|
|
105
|
+
|
|
106
|
+
def dfs(node, fa_idx):
|
|
107
|
+
max_depth = 0
|
|
108
|
+
for child in node.neighbors:
|
|
109
|
+
if child.idx == fa_idx: continue
|
|
110
|
+
max_depth = max(max_depth, dfs(child, node.idx))
|
|
111
|
+
return max_depth + 1
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import rdkit.Chem as Chem
|
|
4
|
+
import torch.nn.functional as F
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from .nnutils import *
|
|
8
|
+
from .chemutils import get_mol
|
|
9
|
+
|
|
10
|
+
ELEM_LIST = ['C', 'N', 'O', 'S', 'F', 'Si', 'P', 'Cl', 'Br', 'Mg', 'Na', 'Ca', 'Fe', 'Al', 'I', 'B', 'K', 'Se', 'Zn', 'H', 'Cu', 'Mn', 'unknown']
|
|
11
|
+
|
|
12
|
+
ATOM_FDIM = len(ELEM_LIST) + 6 + 5 + 4 + 1
|
|
13
|
+
BOND_FDIM = 5 + 6
|
|
14
|
+
MAX_NB = 6
|
|
15
|
+
|
|
16
|
+
def onek_encoding_unk(x, allowable_set):
|
|
17
|
+
if x not in allowable_set:
|
|
18
|
+
x = allowable_set[-1]
|
|
19
|
+
return list(map(lambda s: x == s, allowable_set))
|
|
20
|
+
|
|
21
|
+
def atom_features(atom):
|
|
22
|
+
return torch.Tensor(onek_encoding_unk(atom.GetSymbol(), ELEM_LIST)
|
|
23
|
+
+ onek_encoding_unk(atom.GetDegree(), [0,1,2,3,4,5])
|
|
24
|
+
+ onek_encoding_unk(atom.GetFormalCharge(), [-1,-2,1,2,0])
|
|
25
|
+
+ onek_encoding_unk(int(atom.GetChiralTag()), [0,1,2,3])
|
|
26
|
+
+ [atom.GetIsAromatic()])
|
|
27
|
+
|
|
28
|
+
def bond_features(bond):
|
|
29
|
+
bt = bond.GetBondType()
|
|
30
|
+
stereo = int(bond.GetStereo())
|
|
31
|
+
fbond = [bt == Chem.rdchem.BondType.SINGLE, bt == Chem.rdchem.BondType.DOUBLE, bt == Chem.rdchem.BondType.TRIPLE, bt == Chem.rdchem.BondType.AROMATIC, bond.IsInRing()]
|
|
32
|
+
fstereo = onek_encoding_unk(stereo, [0,1,2,3,4,5])
|
|
33
|
+
return torch.Tensor(fbond + fstereo)
|
|
34
|
+
|
|
35
|
+
class MPN(nn.Module):
|
|
36
|
+
|
|
37
|
+
def __init__(self, hidden_size, depth):
|
|
38
|
+
super(MPN, self).__init__()
|
|
39
|
+
self.hidden_size = hidden_size
|
|
40
|
+
self.depth = depth
|
|
41
|
+
|
|
42
|
+
self.W_i = nn.Linear(ATOM_FDIM + BOND_FDIM, hidden_size, bias=False)
|
|
43
|
+
self.W_h = nn.Linear(hidden_size, hidden_size, bias=False)
|
|
44
|
+
self.W_o = nn.Linear(ATOM_FDIM + hidden_size, hidden_size)
|
|
45
|
+
|
|
46
|
+
def forward(self, fatoms, fbonds, agraph, bgraph, scope):
|
|
47
|
+
fatoms = create_var(fatoms)
|
|
48
|
+
fbonds = create_var(fbonds)
|
|
49
|
+
agraph = create_var(agraph)
|
|
50
|
+
bgraph = create_var(bgraph)
|
|
51
|
+
|
|
52
|
+
binput = self.W_i(fbonds)
|
|
53
|
+
message = F.relu(binput)
|
|
54
|
+
|
|
55
|
+
for i in range(self.depth - 1):
|
|
56
|
+
nei_message = index_select_ND(message, 0, bgraph)
|
|
57
|
+
nei_message = nei_message.sum(dim=1)
|
|
58
|
+
nei_message = self.W_h(nei_message)
|
|
59
|
+
message = F.relu(binput + nei_message)
|
|
60
|
+
|
|
61
|
+
nei_message = index_select_ND(message, 0, agraph)
|
|
62
|
+
nei_message = nei_message.sum(dim=1)
|
|
63
|
+
ainput = torch.cat([fatoms, nei_message], dim=1)
|
|
64
|
+
atom_hiddens = F.relu(self.W_o(ainput))
|
|
65
|
+
|
|
66
|
+
max_len = max([x for _,x in scope])
|
|
67
|
+
batch_vecs = []
|
|
68
|
+
for st,le in scope:
|
|
69
|
+
cur_vecs = atom_hiddens[st : st + le].mean(dim=0)
|
|
70
|
+
batch_vecs.append( cur_vecs )
|
|
71
|
+
|
|
72
|
+
mol_vecs = torch.stack(batch_vecs, dim=0)
|
|
73
|
+
return mol_vecs
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def tensorize(mol_batch):
|
|
77
|
+
padding = torch.zeros(ATOM_FDIM + BOND_FDIM)
|
|
78
|
+
fatoms,fbonds = [],[padding] #Ensure bond is 1-indexed
|
|
79
|
+
in_bonds,all_bonds = [],[(-1,-1)] #Ensure bond is 1-indexed
|
|
80
|
+
scope = []
|
|
81
|
+
total_atoms = 0
|
|
82
|
+
|
|
83
|
+
for smiles in mol_batch:
|
|
84
|
+
mol = get_mol(smiles)
|
|
85
|
+
#mol = Chem.MolFromSmiles(smiles)
|
|
86
|
+
n_atoms = mol.GetNumAtoms()
|
|
87
|
+
for atom in mol.GetAtoms():
|
|
88
|
+
fatoms.append( atom_features(atom) )
|
|
89
|
+
in_bonds.append([])
|
|
90
|
+
|
|
91
|
+
for bond in mol.GetBonds():
|
|
92
|
+
a1 = bond.GetBeginAtom()
|
|
93
|
+
a2 = bond.GetEndAtom()
|
|
94
|
+
x = a1.GetIdx() + total_atoms
|
|
95
|
+
y = a2.GetIdx() + total_atoms
|
|
96
|
+
|
|
97
|
+
b = len(all_bonds)
|
|
98
|
+
all_bonds.append((x,y))
|
|
99
|
+
fbonds.append( torch.cat([fatoms[x], bond_features(bond)], 0) )
|
|
100
|
+
in_bonds[y].append(b)
|
|
101
|
+
|
|
102
|
+
b = len(all_bonds)
|
|
103
|
+
all_bonds.append((y,x))
|
|
104
|
+
fbonds.append( torch.cat([fatoms[y], bond_features(bond)], 0) )
|
|
105
|
+
in_bonds[x].append(b)
|
|
106
|
+
|
|
107
|
+
scope.append((total_atoms,n_atoms))
|
|
108
|
+
total_atoms += n_atoms
|
|
109
|
+
|
|
110
|
+
total_bonds = len(all_bonds)
|
|
111
|
+
fatoms = torch.stack(fatoms, 0)
|
|
112
|
+
fbonds = torch.stack(fbonds, 0)
|
|
113
|
+
agraph = torch.zeros(total_atoms,MAX_NB).long()
|
|
114
|
+
bgraph = torch.zeros(total_bonds,MAX_NB).long()
|
|
115
|
+
|
|
116
|
+
for a in range(total_atoms):
|
|
117
|
+
for i,b in enumerate(in_bonds[a]):
|
|
118
|
+
if i >= MAX_NB: break
|
|
119
|
+
agraph[a,i] = b
|
|
120
|
+
|
|
121
|
+
for b1 in range(1, total_bonds):
|
|
122
|
+
x,y = all_bonds[b1]
|
|
123
|
+
for i,b2 in enumerate(in_bonds[x]):
|
|
124
|
+
if all_bonds[b2][0] != y:
|
|
125
|
+
if i >= MAX_NB: break
|
|
126
|
+
bgraph[b1,i] = b2
|
|
127
|
+
|
|
128
|
+
return (fatoms, fbonds, agraph, bgraph, scope)
|
|
129
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
from torch.autograd import Variable
|
|
5
|
+
|
|
6
|
+
def create_var(tensor, requires_grad=None):
|
|
7
|
+
if requires_grad is None:
|
|
8
|
+
return Variable(tensor).cuda()
|
|
9
|
+
else:
|
|
10
|
+
return Variable(tensor, requires_grad=requires_grad).cuda()
|
|
11
|
+
|
|
12
|
+
def index_select_ND(source, dim, index):
|
|
13
|
+
index_size = index.size()
|
|
14
|
+
suffix_dim = source.size()[1:]
|
|
15
|
+
final_size = index_size + suffix_dim
|
|
16
|
+
target = source.index_select(dim, index.view(-1))
|
|
17
|
+
return target.view(final_size)
|
|
18
|
+
|
|
19
|
+
def avg_pool(all_vecs, scope, dim):
|
|
20
|
+
size = create_var(torch.Tensor([le for _,le in scope]))
|
|
21
|
+
return all_vecs.sum(dim=dim) / size.unsqueeze(-1)
|
|
22
|
+
|
|
23
|
+
def stack_pad_tensor(tensor_list):
|
|
24
|
+
max_len = max([t.size(0) for t in tensor_list])
|
|
25
|
+
for i,tensor in enumerate(tensor_list):
|
|
26
|
+
pad_len = max_len - tensor.size(0)
|
|
27
|
+
tensor_list[i] = F.pad( tensor, (0,0,0,pad_len) )
|
|
28
|
+
return torch.stack(tensor_list, dim=0)
|
|
29
|
+
|
|
30
|
+
#3D padded tensor to 2D matrix, with padded zeros removed
|
|
31
|
+
def flatten_tensor(tensor, scope):
|
|
32
|
+
assert tensor.size(0) == len(scope)
|
|
33
|
+
tlist = []
|
|
34
|
+
for i,tup in enumerate(scope):
|
|
35
|
+
le = tup[1]
|
|
36
|
+
tlist.append( tensor[i, 0:le] )
|
|
37
|
+
return torch.cat(tlist, dim=0)
|
|
38
|
+
|
|
39
|
+
#2D matrix to 3D padded tensor
|
|
40
|
+
def inflate_tensor(tensor, scope):
|
|
41
|
+
max_len = max([le for _,le in scope])
|
|
42
|
+
batch_vecs = []
|
|
43
|
+
for st,le in scope:
|
|
44
|
+
cur_vecs = tensor[st : st + le]
|
|
45
|
+
cur_vecs = F.pad( cur_vecs, (0,0,0,max_len-le) )
|
|
46
|
+
batch_vecs.append( cur_vecs )
|
|
47
|
+
|
|
48
|
+
return torch.stack(batch_vecs, dim=0)
|
|
49
|
+
|
|
50
|
+
def GRU(x, h_nei, W_z, W_r, U_r, W_h):
|
|
51
|
+
hidden_size = x.size()[-1]
|
|
52
|
+
sum_h = h_nei.sum(dim=1)
|
|
53
|
+
z_input = torch.cat([x,sum_h], dim=1)
|
|
54
|
+
z = F.sigmoid(W_z(z_input))
|
|
55
|
+
|
|
56
|
+
r_1 = W_r(x).view(-1,1,hidden_size)
|
|
57
|
+
r_2 = U_r(h_nei)
|
|
58
|
+
r = F.sigmoid(r_1 + r_2)
|
|
59
|
+
|
|
60
|
+
gated_h = r * h_nei
|
|
61
|
+
sum_gated_h = gated_h.sum(dim=1)
|
|
62
|
+
h_input = torch.cat([x,sum_gated_h], dim=1)
|
|
63
|
+
pre_h = F.tanh(W_h(h_input))
|
|
64
|
+
new_h = (1.0 - z) * sum_h + z * pre_h
|
|
65
|
+
return new_h
|
|
66
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import rdkit
|
|
2
|
+
import rdkit.Chem as Chem
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
def get_slots(smiles):
|
|
6
|
+
mol = Chem.MolFromSmiles(smiles)
|
|
7
|
+
return [(atom.GetSymbol(), atom.GetFormalCharge(), atom.GetTotalNumHs()) for atom in mol.GetAtoms()]
|
|
8
|
+
|
|
9
|
+
class Vocab(object):
|
|
10
|
+
benzynes = ['C1=CC=CC=C1', 'C1=CC=NC=C1', 'C1=CC=NN=C1', 'C1=CN=CC=N1', 'C1=CN=CN=C1', 'C1=CN=NC=N1', 'C1=CN=NN=C1', 'C1=NC=NC=N1', 'C1=NN=CN=N1']
|
|
11
|
+
penzynes = ['C1=C[NH]C=C1', 'C1=C[NH]C=N1', 'C1=C[NH]N=C1', 'C1=C[NH]N=N1', 'C1=COC=C1', 'C1=COC=N1', 'C1=CON=C1', 'C1=CSC=C1', 'C1=CSC=N1', 'C1=CSN=C1', 'C1=CSN=N1', 'C1=NN=C[NH]1', 'C1=NN=CO1', 'C1=NN=CS1', 'C1=N[NH]C=N1', 'C1=N[NH]N=C1', 'C1=N[NH]N=N1', 'C1=NN=N[NH]1', 'C1=NN=NS1', 'C1=NOC=N1', 'C1=NON=C1', 'C1=NSC=N1', 'C1=NSN=C1']
|
|
12
|
+
|
|
13
|
+
def __init__(self, smiles_list):
|
|
14
|
+
self.vocab = smiles_list
|
|
15
|
+
self.vmap = {x:i for i,x in enumerate(self.vocab)}
|
|
16
|
+
self.slots = [get_slots(smiles) for smiles in self.vocab]
|
|
17
|
+
Vocab.benzynes = [s for s in smiles_list if s.count('=') >= 2 and Chem.MolFromSmiles(s).GetNumAtoms() == 6] + ['C1=CCNCC1']
|
|
18
|
+
Vocab.penzynes = [s for s in smiles_list if s.count('=') >= 2 and Chem.MolFromSmiles(s).GetNumAtoms() == 5] + ['C1=NCCN1','C1=NNCC1']
|
|
19
|
+
|
|
20
|
+
def get_index(self, smiles):
|
|
21
|
+
return self.vmap[smiles]
|
|
22
|
+
|
|
23
|
+
def get_smiles(self, idx):
|
|
24
|
+
return self.vocab[idx]
|
|
25
|
+
|
|
26
|
+
def get_slots(self, idx):
|
|
27
|
+
return copy.deepcopy(self.slots[idx])
|
|
28
|
+
|
|
29
|
+
def size(self):
|
|
30
|
+
return len(self.vocab)
|
|
31
|
+
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
import torch.nn.functional as F
|
|
4
|
+
import copy
|
|
5
|
+
import rdkit
|
|
6
|
+
import rdkit.Chem as Chem
|
|
7
|
+
|
|
8
|
+
from .jtnn.mol_tree import Vocab, MolTree
|
|
9
|
+
from .jtnn.nnutils import create_var, flatten_tensor, avg_pool
|
|
10
|
+
from .jtnn.jtnn_enc import JTNNEncoder
|
|
11
|
+
from .jtnn.jtnn_dec import JTNNDecoder
|
|
12
|
+
from .jtnn.mpn import MPN
|
|
13
|
+
from .jtnn.jtmpn import JTMPN
|
|
14
|
+
from .jtnn.datautils import tensorize
|
|
15
|
+
from .jtnn.chemutils import enum_assemble, set_atommap, copy_edit_mol, attach_mols
|
|
16
|
+
|
|
17
|
+
class JTNNVAE(nn.Module):
|
|
18
|
+
def __init__(self, vocab, hidden_size, latent_size, depthT, depthG):
|
|
19
|
+
super(JTNNVAE, self).__init__()
|
|
20
|
+
self.hidden_size = hidden_size
|
|
21
|
+
assert latent_size % 2 == 0, 'latent_size must be even'
|
|
22
|
+
self.latent_size = latent_size = int(latent_size / 2) #Tree and Mol has two vectors
|
|
23
|
+
|
|
24
|
+
if vocab is None:
|
|
25
|
+
raise ValueError('JTNNVAE is not properly initialized. vocab is required')
|
|
26
|
+
|
|
27
|
+
self.vocab = vocab
|
|
28
|
+
self.jtnn = JTNNEncoder(hidden_size, depthT, nn.Embedding(vocab.size(), hidden_size))
|
|
29
|
+
self.decoder = JTNNDecoder(vocab, hidden_size, latent_size, nn.Embedding(vocab.size(), hidden_size))
|
|
30
|
+
|
|
31
|
+
self.jtmpn = JTMPN(hidden_size, depthG)
|
|
32
|
+
self.mpn = MPN(hidden_size, depthG)
|
|
33
|
+
|
|
34
|
+
self.A_assm = nn.Linear(latent_size, hidden_size, bias=False)
|
|
35
|
+
self.assm_loss = nn.CrossEntropyLoss(reduction='sum')
|
|
36
|
+
|
|
37
|
+
self.T_mean = nn.Linear(hidden_size, latent_size)
|
|
38
|
+
self.T_var = nn.Linear(hidden_size, latent_size)
|
|
39
|
+
self.G_mean = nn.Linear(hidden_size, latent_size)
|
|
40
|
+
self.G_var = nn.Linear(hidden_size, latent_size)
|
|
41
|
+
|
|
42
|
+
self.initialize_parameters()
|
|
43
|
+
|
|
44
|
+
def initialize_parameters(self, seed=None):
|
|
45
|
+
if seed is not None:
|
|
46
|
+
torch.manual_seed(seed)
|
|
47
|
+
|
|
48
|
+
def _basic_init(module):
|
|
49
|
+
if isinstance(module, nn.Linear):
|
|
50
|
+
torch.nn.init.xavier_uniform_(module.weight)
|
|
51
|
+
if module.bias is not None:
|
|
52
|
+
nn.init.constant_(module.bias, 0)
|
|
53
|
+
|
|
54
|
+
self.apply(_basic_init)
|
|
55
|
+
|
|
56
|
+
def compute_loss(self, x_batch):
|
|
57
|
+
word_loss, topo_loss, assm_loss, kl_div, _, _, _ = self.forward(x_batch)
|
|
58
|
+
return word_loss, topo_loss, assm_loss, kl_div
|
|
59
|
+
|
|
60
|
+
def encode(self, jtenc_holder, mpn_holder):
|
|
61
|
+
tree_vecs, tree_mess = self.jtnn(*jtenc_holder)
|
|
62
|
+
mol_vecs = self.mpn(*mpn_holder)
|
|
63
|
+
return tree_vecs, tree_mess, mol_vecs
|
|
64
|
+
|
|
65
|
+
def encode_from_smiles(self, smiles_list):
|
|
66
|
+
tree_batch = [MolTree(s) for s in smiles_list]
|
|
67
|
+
_, jtenc_holder, mpn_holder = tensorize(tree_batch, self.vocab, assm=False)
|
|
68
|
+
tree_vecs, _, mol_vecs = self.encode(jtenc_holder, mpn_holder)
|
|
69
|
+
return torch.cat([tree_vecs, mol_vecs], dim=-1)
|
|
70
|
+
|
|
71
|
+
def encode_latent(self, jtenc_holder, mpn_holder):
|
|
72
|
+
tree_vecs, _ = self.jtnn(*jtenc_holder)
|
|
73
|
+
mol_vecs = self.mpn(*mpn_holder)
|
|
74
|
+
tree_mean = self.T_mean(tree_vecs)
|
|
75
|
+
mol_mean = self.G_mean(mol_vecs)
|
|
76
|
+
tree_var = -torch.abs(self.T_var(tree_vecs))
|
|
77
|
+
mol_var = -torch.abs(self.G_var(mol_vecs))
|
|
78
|
+
return torch.cat([tree_mean, mol_mean], dim=1), torch.cat([tree_var, mol_var], dim=1)
|
|
79
|
+
|
|
80
|
+
def rsample(self, z_vecs, W_mean, W_var):
|
|
81
|
+
batch_size = z_vecs.size(0)
|
|
82
|
+
z_mean = W_mean(z_vecs)
|
|
83
|
+
z_log_var = -torch.abs(W_var(z_vecs)) # Following Mueller et al.
|
|
84
|
+
kl_loss = -0.5 * torch.sum(1.0 + z_log_var - z_mean * z_mean - torch.exp(z_log_var)) / batch_size
|
|
85
|
+
epsilon = create_var(torch.randn_like(z_mean))
|
|
86
|
+
z_vecs = z_mean + torch.exp(z_log_var / 2) * epsilon
|
|
87
|
+
return z_vecs, kl_loss
|
|
88
|
+
|
|
89
|
+
def sample_prior(self, device, prob_decode=False):
|
|
90
|
+
z_tree = torch.randn(1, self.latent_size).to(device)
|
|
91
|
+
z_mol = torch.randn(1, self.latent_size).to(device)
|
|
92
|
+
return self.decode(z_tree, z_mol, prob_decode, device)
|
|
93
|
+
|
|
94
|
+
def forward(self, x_batch):
|
|
95
|
+
x_batch, x_jtenc_holder, x_mpn_holder, x_jtmpn_holder = x_batch
|
|
96
|
+
x_tree_vecs, x_tree_mess, x_mol_vecs = self.encode(x_jtenc_holder, x_mpn_holder)
|
|
97
|
+
z_tree_vecs, tree_kl = self.rsample(x_tree_vecs, self.T_mean, self.T_var)
|
|
98
|
+
z_mol_vecs, mol_kl = self.rsample(x_mol_vecs, self.G_mean, self.G_var)
|
|
99
|
+
|
|
100
|
+
kl_div = tree_kl + mol_kl
|
|
101
|
+
word_loss, topo_loss, word_acc, topo_acc = self.decoder(x_batch, z_tree_vecs)
|
|
102
|
+
assm_loss, assm_acc = self.assm(x_batch, x_jtmpn_holder, z_mol_vecs, x_tree_mess)
|
|
103
|
+
|
|
104
|
+
# return word_loss + topo_loss + assm_loss + beta * kl_div, kl_div.item(), word_acc, topo_acc, assm_acc
|
|
105
|
+
return word_loss, topo_loss, assm_loss, kl_div, word_acc, topo_acc, assm_acc
|
|
106
|
+
|
|
107
|
+
def assm(self, mol_batch, jtmpn_holder, x_mol_vecs, x_tree_mess):
|
|
108
|
+
jtmpn_holder,batch_idx = jtmpn_holder
|
|
109
|
+
fatoms,fbonds,agraph,bgraph,scope = jtmpn_holder
|
|
110
|
+
batch_idx = create_var(batch_idx)
|
|
111
|
+
|
|
112
|
+
cand_vecs = self.jtmpn(fatoms, fbonds, agraph, bgraph, scope, x_tree_mess)
|
|
113
|
+
|
|
114
|
+
x_mol_vecs = x_mol_vecs.index_select(0, batch_idx)
|
|
115
|
+
x_mol_vecs = self.A_assm(x_mol_vecs) #bilinear
|
|
116
|
+
scores = torch.bmm(
|
|
117
|
+
x_mol_vecs.unsqueeze(1),
|
|
118
|
+
cand_vecs.unsqueeze(-1)
|
|
119
|
+
).squeeze()
|
|
120
|
+
|
|
121
|
+
cnt,tot,acc = 0,0,0
|
|
122
|
+
all_loss = []
|
|
123
|
+
for i,mol_tree in enumerate(mol_batch):
|
|
124
|
+
comp_nodes = [node for node in mol_tree.nodes if len(node.cands) > 1 and not node.is_leaf]
|
|
125
|
+
cnt += len(comp_nodes)
|
|
126
|
+
for node in comp_nodes:
|
|
127
|
+
label = node.cands.index(node.label)
|
|
128
|
+
ncand = len(node.cands)
|
|
129
|
+
cur_score = scores.narrow(0, tot, ncand)
|
|
130
|
+
tot += ncand
|
|
131
|
+
|
|
132
|
+
if cur_score.data[label] >= cur_score.max().item():
|
|
133
|
+
acc += 1
|
|
134
|
+
|
|
135
|
+
label = create_var(torch.LongTensor([label]))
|
|
136
|
+
all_loss.append( self.assm_loss(cur_score.view(1,-1), label) )
|
|
137
|
+
|
|
138
|
+
all_loss = sum(all_loss) / len(mol_batch)
|
|
139
|
+
return all_loss, acc * 1.0 / cnt
|
|
140
|
+
|
|
141
|
+
def decode(self, x_tree_vecs, x_mol_vecs, prob_decode, device):
|
|
142
|
+
#currently do not support batch decoding
|
|
143
|
+
assert x_tree_vecs.size(0) == 1 and x_mol_vecs.size(0) == 1
|
|
144
|
+
|
|
145
|
+
pred_root, pred_nodes = self.decoder.decode(x_tree_vecs, prob_decode)
|
|
146
|
+
if len(pred_nodes) == 0: return None
|
|
147
|
+
elif len(pred_nodes) == 1: return pred_root.smiles
|
|
148
|
+
|
|
149
|
+
#Mark nid & is_leaf & atommap
|
|
150
|
+
for i, node in enumerate(pred_nodes):
|
|
151
|
+
node.nid = i + 1
|
|
152
|
+
node.is_leaf = (len(node.neighbors) == 1)
|
|
153
|
+
if len(node.neighbors) > 1:
|
|
154
|
+
set_atommap(node.mol, node.nid)
|
|
155
|
+
|
|
156
|
+
scope = [(0, len(pred_nodes))]
|
|
157
|
+
jtenc_holder,mess_dict = JTNNEncoder.tensorize_nodes(pred_nodes, scope)
|
|
158
|
+
_, tree_mess = self.jtnn(*jtenc_holder)
|
|
159
|
+
tree_mess = (tree_mess, mess_dict) #Important: tree_mess is a matrix, mess_dict is a python dict
|
|
160
|
+
|
|
161
|
+
x_mol_vecs = self.A_assm(x_mol_vecs).squeeze() #bilinear
|
|
162
|
+
|
|
163
|
+
cur_mol = copy_edit_mol(pred_root.mol)
|
|
164
|
+
global_amap = [{}] + [{} for node in pred_nodes]
|
|
165
|
+
global_amap[1] = {atom.GetIdx():atom.GetIdx() for atom in cur_mol.GetAtoms()}
|
|
166
|
+
|
|
167
|
+
cur_mol,_ = self.dfs_assemble(tree_mess, x_mol_vecs, pred_nodes, cur_mol, global_amap, [], pred_root, None, prob_decode, check_aroma=True, device=device)
|
|
168
|
+
if cur_mol is None:
|
|
169
|
+
cur_mol = copy_edit_mol(pred_root.mol)
|
|
170
|
+
global_amap = [{}] + [{} for node in pred_nodes]
|
|
171
|
+
global_amap[1] = {atom.GetIdx():atom.GetIdx() for atom in cur_mol.GetAtoms()}
|
|
172
|
+
cur_mol, pre_mol = self.dfs_assemble(tree_mess, x_mol_vecs, pred_nodes, cur_mol, global_amap, [], pred_root, None, prob_decode, check_aroma=False, device=device)
|
|
173
|
+
if cur_mol is None: cur_mol = pre_mol
|
|
174
|
+
|
|
175
|
+
if cur_mol is None:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
cur_mol = cur_mol.GetMol()
|
|
179
|
+
set_atommap(cur_mol)
|
|
180
|
+
cur_mol = Chem.MolFromSmiles(Chem.MolToSmiles(cur_mol))
|
|
181
|
+
return Chem.MolToSmiles(cur_mol) if cur_mol is not None else None
|
|
182
|
+
|
|
183
|
+
def dfs_assemble(self, y_tree_mess, x_mol_vecs, all_nodes, cur_mol, global_amap, fa_amap, cur_node, fa_node, prob_decode, check_aroma, device):
|
|
184
|
+
fa_nid = fa_node.nid if fa_node is not None else -1
|
|
185
|
+
prev_nodes = [fa_node] if fa_node is not None else []
|
|
186
|
+
|
|
187
|
+
children = [nei for nei in cur_node.neighbors if nei.nid != fa_nid]
|
|
188
|
+
neighbors = [nei for nei in children if nei.mol.GetNumAtoms() > 1]
|
|
189
|
+
neighbors = sorted(neighbors, key=lambda x:x.mol.GetNumAtoms(), reverse=True)
|
|
190
|
+
singletons = [nei for nei in children if nei.mol.GetNumAtoms() == 1]
|
|
191
|
+
neighbors = singletons + neighbors
|
|
192
|
+
|
|
193
|
+
cur_amap = [(fa_nid,a2,a1) for nid,a1,a2 in fa_amap if nid == cur_node.nid]
|
|
194
|
+
cands,aroma_score = enum_assemble(cur_node, neighbors, prev_nodes, cur_amap)
|
|
195
|
+
if len(cands) == 0 or (sum(aroma_score) < 0 and check_aroma):
|
|
196
|
+
return None, cur_mol
|
|
197
|
+
|
|
198
|
+
cand_smiles,cand_amap = list(zip(*cands))
|
|
199
|
+
aroma_score = torch.Tensor(aroma_score).to(device)
|
|
200
|
+
cands = [(smiles, all_nodes, cur_node) for smiles in cand_smiles]
|
|
201
|
+
|
|
202
|
+
if len(cands) > 1:
|
|
203
|
+
jtmpn_holder = JTMPN.tensorize(cands, y_tree_mess[1])
|
|
204
|
+
fatoms,fbonds,agraph,bgraph,scope = jtmpn_holder
|
|
205
|
+
cand_vecs = self.jtmpn(fatoms, fbonds, agraph, bgraph, scope, y_tree_mess[0])
|
|
206
|
+
scores = torch.mv(cand_vecs, x_mol_vecs) + aroma_score
|
|
207
|
+
else:
|
|
208
|
+
scores = torch.Tensor([1.0])
|
|
209
|
+
|
|
210
|
+
if prob_decode:
|
|
211
|
+
probs = F.softmax(scores.view(1,-1), dim=1).squeeze() + 1e-7 #prevent prob = 0
|
|
212
|
+
cand_idx = torch.multinomial(probs, probs.numel())
|
|
213
|
+
else:
|
|
214
|
+
_, cand_idx = torch.sort(scores, descending=True)
|
|
215
|
+
|
|
216
|
+
backup_mol = Chem.RWMol(cur_mol)
|
|
217
|
+
pre_mol = cur_mol
|
|
218
|
+
for i in range(cand_idx.numel()):
|
|
219
|
+
cur_mol = Chem.RWMol(backup_mol)
|
|
220
|
+
pred_amap = cand_amap[cand_idx[i].item()]
|
|
221
|
+
new_global_amap = copy.deepcopy(global_amap)
|
|
222
|
+
|
|
223
|
+
for nei_id,ctr_atom,nei_atom in pred_amap:
|
|
224
|
+
if nei_id == fa_nid:
|
|
225
|
+
continue
|
|
226
|
+
new_global_amap[nei_id][nei_atom] = new_global_amap[cur_node.nid][ctr_atom]
|
|
227
|
+
|
|
228
|
+
cur_mol = attach_mols(cur_mol, children, [], new_global_amap) #father is already attached
|
|
229
|
+
new_mol = cur_mol.GetMol()
|
|
230
|
+
new_mol = Chem.MolFromSmiles(Chem.MolToSmiles(new_mol))
|
|
231
|
+
|
|
232
|
+
if new_mol is None: continue
|
|
233
|
+
|
|
234
|
+
has_error = False
|
|
235
|
+
for nei_node in children:
|
|
236
|
+
if nei_node.is_leaf: continue
|
|
237
|
+
tmp_mol, tmp_mol2 = self.dfs_assemble(y_tree_mess, x_mol_vecs, all_nodes, cur_mol, new_global_amap, pred_amap, nei_node, cur_node, prob_decode, check_aroma, device)
|
|
238
|
+
if tmp_mol is None:
|
|
239
|
+
has_error = True
|
|
240
|
+
if i == 0: pre_mol = tmp_mol2
|
|
241
|
+
break
|
|
242
|
+
cur_mol = tmp_mol
|
|
243
|
+
|
|
244
|
+
if not has_error: return cur_mol, cur_mol
|
|
245
|
+
|
|
246
|
+
return None, pre_mol
|