aamutils 0.0.1__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.
- aamutils/__init__.py +0 -0
- aamutils/__main__.py +18 -0
- aamutils/algorithm/__init__.py +0 -0
- aamutils/algorithm/aaming.py +124 -0
- aamutils/algorithm/ilp.py +150 -0
- aamutils/cmd/__init__.py +15 -0
- aamutils/cmd/expanding.py +84 -0
- aamutils/utils.py +125 -0
- aamutils-0.0.1.dist-info/METADATA +86 -0
- aamutils-0.0.1.dist-info/RECORD +12 -0
- aamutils-0.0.1.dist-info/WHEEL +4 -0
- aamutils-0.0.1.dist-info/licenses/LICENSE +9 -0
aamutils/__init__.py
ADDED
|
File without changes
|
aamutils/__main__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from aamutils.cmd import setup_argparser
|
|
4
|
+
|
|
5
|
+
if __name__ == "__main__":
|
|
6
|
+
logger = logging.getLogger("aamutils")
|
|
7
|
+
ch = logging.StreamHandler()
|
|
8
|
+
ch.setLevel(logging.INFO)
|
|
9
|
+
formatter = logging.Formatter(
|
|
10
|
+
"[%(asctime)s %(levelname)s] %(message)s", datefmt="%y-%m-%d %H:%M:%S"
|
|
11
|
+
)
|
|
12
|
+
ch.setFormatter(formatter)
|
|
13
|
+
logger.setLevel(logging.INFO)
|
|
14
|
+
logger.addHandler(ch)
|
|
15
|
+
|
|
16
|
+
parser = setup_argparser()
|
|
17
|
+
args = parser.parse_args()
|
|
18
|
+
args.func(args)
|
|
File without changes
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import networkx as nx
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _add_its_nodes(ITS, G, H, eta, symbol_key):
|
|
6
|
+
eta_G, eta_G_inv, eta_H, eta_H_inv = eta[0], eta[1], eta[2], eta[3]
|
|
7
|
+
for n, d in G.nodes(data=True):
|
|
8
|
+
n_ITS = eta_G[n]
|
|
9
|
+
n_H = eta_H_inv[n_ITS]
|
|
10
|
+
if n_ITS is not None and n_H is not None:
|
|
11
|
+
ITS.add_node(n_ITS, symbol=d[symbol_key], idx_map=(n, n_H))
|
|
12
|
+
for n, d in H.nodes(data=True):
|
|
13
|
+
n_ITS = eta_H[n]
|
|
14
|
+
n_G = eta_G_inv[n_ITS]
|
|
15
|
+
if n_ITS is not None and n_G is not None and n_ITS not in ITS.nodes:
|
|
16
|
+
ITS.add_node(n_ITS, symbol=d[symbol_key], idx_map=(n_G, n))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _add_its_edges(ITS, G, H, eta, bond_key):
|
|
20
|
+
eta_G, eta_G_inv, eta_H, eta_H_inv = eta[0], eta[1], eta[2], eta[3]
|
|
21
|
+
for n1, n2, d in G.edges(data=True):
|
|
22
|
+
if n1 > n2:
|
|
23
|
+
continue
|
|
24
|
+
e_G = d[bond_key]
|
|
25
|
+
n_ITS1 = eta_G[n1]
|
|
26
|
+
n_ITS2 = eta_G[n2]
|
|
27
|
+
n_H1 = eta_H_inv[n_ITS1]
|
|
28
|
+
n_H2 = eta_H_inv[n_ITS2]
|
|
29
|
+
e_H = None
|
|
30
|
+
if H.has_edge(n_H1, n_H2):
|
|
31
|
+
e_H = H[n_H1][n_H2][bond_key]
|
|
32
|
+
if not ITS.has_edge(n_ITS1, n_ITS2) and n_ITS1 > 0 and n_ITS2 > 0:
|
|
33
|
+
ITS.add_edge(n_ITS1, n_ITS2, bond=(e_G, e_H))
|
|
34
|
+
|
|
35
|
+
for n1, n2, d in H.edges(data=True):
|
|
36
|
+
if n1 > n2:
|
|
37
|
+
continue
|
|
38
|
+
e_H = d[bond_key]
|
|
39
|
+
n_ITS1 = eta_H[n1]
|
|
40
|
+
n_ITS2 = eta_H[n2]
|
|
41
|
+
n_G1 = eta_G_inv[n_ITS1]
|
|
42
|
+
n_G2 = eta_G_inv[n_ITS2]
|
|
43
|
+
if n_G1 is None or n_G2 is None:
|
|
44
|
+
continue
|
|
45
|
+
if not G.has_edge(n_G1, n_G2) and n_ITS1 > 0 and n_ITS2 > 0:
|
|
46
|
+
ITS.add_edge(n_ITS1, n_ITS2, bond=(None, e_H))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_its(
|
|
50
|
+
G: nx.Graph, H: nx.Graph, aam_key="aam", symbol_key="symbol", bond_key="bond"
|
|
51
|
+
) -> nx.Graph:
|
|
52
|
+
"""Get the ITS graph of reaction G \u2192 H.
|
|
53
|
+
|
|
54
|
+
:param G: Educt molecular graph.
|
|
55
|
+
:param H: Product molecular graph.
|
|
56
|
+
:param aam_key: (optional) The node label that encodes atom indices on the
|
|
57
|
+
molecular graph.
|
|
58
|
+
:param symbol_key: (optional) The node label that encodes atom symbols on
|
|
59
|
+
the molecular graph.
|
|
60
|
+
:param bond_key: (optional) The edge label that encodes bond order on the
|
|
61
|
+
molecular graph.
|
|
62
|
+
|
|
63
|
+
:returns: Returns the ITS graph.
|
|
64
|
+
"""
|
|
65
|
+
eta_G = collections.defaultdict(lambda: None)
|
|
66
|
+
eta_G_inv = collections.defaultdict(lambda: None)
|
|
67
|
+
eta_H = collections.defaultdict(lambda: None)
|
|
68
|
+
eta_H_inv = collections.defaultdict(lambda: None)
|
|
69
|
+
eta = (eta_G, eta_G_inv, eta_H, eta_H_inv)
|
|
70
|
+
|
|
71
|
+
for n, d in G.nodes(data=True):
|
|
72
|
+
if aam_key in d.keys() and d[aam_key] >= 0:
|
|
73
|
+
eta_G[n] = d[aam_key]
|
|
74
|
+
eta_G_inv[d[aam_key]] = n
|
|
75
|
+
for n, d in H.nodes(data=True):
|
|
76
|
+
if aam_key in d.keys() and d[aam_key] >= 0:
|
|
77
|
+
eta_H[n] = d[aam_key]
|
|
78
|
+
eta_H_inv[d[aam_key]] = n
|
|
79
|
+
|
|
80
|
+
ITS = nx.Graph()
|
|
81
|
+
_add_its_nodes(ITS, G, H, eta, symbol_key)
|
|
82
|
+
_add_its_edges(ITS, G, H, eta, bond_key)
|
|
83
|
+
|
|
84
|
+
return ITS
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_rc(ITS: nx.Graph, symbol_key="symbol", bond_key="bond") -> nx.Graph:
|
|
88
|
+
"""Get the reaction center (RC) graph from an ITS graph.
|
|
89
|
+
|
|
90
|
+
:param ITS: The ITS graph to get the RC from.
|
|
91
|
+
:param symbol_key: (optional) The node label that encodes atom symbols on
|
|
92
|
+
the molecular graph.
|
|
93
|
+
:param bond_key: (optional) The edge label that encodes bond order on the
|
|
94
|
+
molecular graph.
|
|
95
|
+
|
|
96
|
+
:returns: Returns the reaction center (RC) graph.
|
|
97
|
+
"""
|
|
98
|
+
rc = nx.Graph()
|
|
99
|
+
for n1, n2, d in ITS.edges(data=True):
|
|
100
|
+
edge_label = d[bond_key]
|
|
101
|
+
if edge_label[0] != edge_label[1]:
|
|
102
|
+
rc.add_node(n1, **{symbol_key: ITS.nodes[n1][symbol_key]})
|
|
103
|
+
rc.add_node(n2, **{symbol_key: ITS.nodes[n2][symbol_key]})
|
|
104
|
+
rc.add_edge(n1, n2, **{bond_key: edge_label})
|
|
105
|
+
return rc
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def is_rc_valid(RC: nx.Graph, symbol_key="symbol") -> bool:
|
|
109
|
+
"""Checks if a reaction center (RC) graph is valid. A valid RC does not
|
|
110
|
+
mean it is chemically correct. This function only checks the necessary
|
|
111
|
+
structural requirements for a correct RC. A chemically correct RC is always
|
|
112
|
+
valid but a valid RC is not necessarily chemically correct.
|
|
113
|
+
|
|
114
|
+
:param symbol_key: (optional) The node label that encodes atom symbols on
|
|
115
|
+
the molecular graph.
|
|
116
|
+
|
|
117
|
+
:returns: Returns ``True`` if the RC graph is valid, ``False`` otherwise.
|
|
118
|
+
"""
|
|
119
|
+
for n, d in RC.degree():
|
|
120
|
+
if d < 2:
|
|
121
|
+
return False, "Atom {} {} has degree {}.".format(
|
|
122
|
+
n, RC.nodes[n][symbol_key], d
|
|
123
|
+
)
|
|
124
|
+
return True
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pulp as lp
|
|
3
|
+
import networkx as nx
|
|
4
|
+
|
|
5
|
+
from aamutils.utils import get_beta_map
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _bijection_constraint(problem, X):
|
|
9
|
+
m = int(np.sqrt(len(X.keys())))
|
|
10
|
+
|
|
11
|
+
for j in range(m):
|
|
12
|
+
problem += (
|
|
13
|
+
lp.lpSum([X[i, j] for i in range(m)]) == 1,
|
|
14
|
+
"bijection_col_{}".format(j),
|
|
15
|
+
)
|
|
16
|
+
for i in range(m):
|
|
17
|
+
problem += (
|
|
18
|
+
lp.lpSum([X[i, j] for j in range(m)]) == 1,
|
|
19
|
+
"bijection_row_{}".format(i),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _beta_constraint(problem, X, beta_map):
|
|
24
|
+
for bi, bj, _ in beta_map:
|
|
25
|
+
problem += (X[bi, bj] == 1, "beta_constraint1_{}-{}".format(bi, bj))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _edge_diff_constraint(problem, X, D, A_G, A_H):
|
|
29
|
+
m = int(np.sqrt(len(D.keys())))
|
|
30
|
+
for i in range(m):
|
|
31
|
+
for j in range(m):
|
|
32
|
+
problem += D[i, j] == lp.lpSum(
|
|
33
|
+
[X[i, k] * A_H[k, j] for k in range(m)]
|
|
34
|
+
) - lp.lpSum([A_G[i, k] * X[k, j] for k in range(m)])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _atom_type_constraint(problem, X, G, H, symbol_key="symbol"):
|
|
38
|
+
m = int(np.sqrt(len(X.keys())))
|
|
39
|
+
assert m == len(G.nodes)
|
|
40
|
+
assert m == len(H.nodes)
|
|
41
|
+
for i in range(m):
|
|
42
|
+
for j in range(m):
|
|
43
|
+
g_sym = G.nodes(data=True)[i][symbol_key]
|
|
44
|
+
h_sym = H.nodes(data=True)[j][symbol_key]
|
|
45
|
+
assert isinstance(g_sym, str)
|
|
46
|
+
assert isinstance(h_sym, str)
|
|
47
|
+
if g_sym != h_sym:
|
|
48
|
+
problem += (X[i, j] == 0, "atom_type_constraint_{}-{}".format(i, j))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _indicator_constraint(problem, D, G, S, k):
|
|
52
|
+
m = int(np.sqrt(len(D.keys())))
|
|
53
|
+
assert m == int(np.sqrt(len(G.keys())))
|
|
54
|
+
assert m == int(np.sqrt(len(S.keys())))
|
|
55
|
+
|
|
56
|
+
for i in range(m):
|
|
57
|
+
for j in range(m):
|
|
58
|
+
problem += (
|
|
59
|
+
D[i, j] <= k * G[i, j],
|
|
60
|
+
"g_indicator_constraint_lower_{}-{}".format(i, j),
|
|
61
|
+
)
|
|
62
|
+
problem += (
|
|
63
|
+
-D[i, j] <= k * S[i, j],
|
|
64
|
+
"s_indicator_constraint_lower_{}-{}".format(i, j),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def expand_partial_aam_balanced(
|
|
69
|
+
G: nx.Graph,
|
|
70
|
+
H: nx.Graph,
|
|
71
|
+
beta_map: None | list[tuple[int, int, int]] = None,
|
|
72
|
+
bond_key="bond",
|
|
73
|
+
) -> tuple[np.ndarray, str, float]:
|
|
74
|
+
"""Function to extend the partial atom-atom map beta of the balanced
|
|
75
|
+
reaction G \u2192 H to a full atom-atom map based on the Minimal Chemical
|
|
76
|
+
Distance (minimize the number of changing bonds).
|
|
77
|
+
|
|
78
|
+
:param G: Educt molecular graph.
|
|
79
|
+
:param H: Product molecular graph.
|
|
80
|
+
:param beta_map: (optional) A list of predefined atom-atom mappings. If not
|
|
81
|
+
specified the existing mapping in G and H will be used.
|
|
82
|
+
:param bond_key: (optional) The edge label in G and H which encodes the
|
|
83
|
+
bond order.
|
|
84
|
+
|
|
85
|
+
:returns: A 3-tuple. The first element is the mapping matrix X. The second
|
|
86
|
+
element is the status string of the ILP solver (e.g. 'Optimal') and the
|
|
87
|
+
third is the ILP solution value.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
A_G = nx.adjacency_matrix(G, weight=bond_key).todense()
|
|
91
|
+
A_H = nx.adjacency_matrix(H, weight=bond_key).todense()
|
|
92
|
+
k = np.max([np.max(A_G), np.max(A_H)])
|
|
93
|
+
m = len(G.nodes)
|
|
94
|
+
|
|
95
|
+
if m != len(H.nodes):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
(
|
|
98
|
+
"Reaction is not balanced. " + "{} reactant atoms and {} product atoms."
|
|
99
|
+
).format(len(G.nodes), len(H.nodes))
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if beta_map is None:
|
|
103
|
+
beta_map = get_beta_map(G, H)
|
|
104
|
+
|
|
105
|
+
# Mapping matrix X
|
|
106
|
+
lp_X = lp.LpVariable.dicts(
|
|
107
|
+
"X",
|
|
108
|
+
[(i, j) for i in range(m) for j in range(m)],
|
|
109
|
+
cat=lp.LpBinary,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Difference matrix D
|
|
113
|
+
lp_D = lp.LpVariable.dicts("D", [(i, j) for i in range(m) for j in range(m)])
|
|
114
|
+
|
|
115
|
+
# Indicator matrix G and S
|
|
116
|
+
lp_G = lp.LpVariable.dicts(
|
|
117
|
+
"G",
|
|
118
|
+
[(i, j) for i in range(m) for j in range(m)],
|
|
119
|
+
cat=lp.LpBinary,
|
|
120
|
+
)
|
|
121
|
+
lp_S = lp.LpVariable.dicts(
|
|
122
|
+
"S",
|
|
123
|
+
[(i, j) for i in range(m) for j in range(m)],
|
|
124
|
+
cat=lp.LpBinary,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Non-zero counter function for D
|
|
128
|
+
lp_f = lp.LpVariable("f", 0, None, lp.LpInteger)
|
|
129
|
+
|
|
130
|
+
problem = lp.LpProblem("AAM", lp.LpMinimize)
|
|
131
|
+
|
|
132
|
+
problem += (lp_f, "objective")
|
|
133
|
+
problem += (
|
|
134
|
+
lp_f == lp.lpSum([lp_G[i, j] + lp_S[i, j] for i in range(m) for j in range(m)]),
|
|
135
|
+
"objective",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
_bijection_constraint(problem, lp_X)
|
|
139
|
+
_beta_constraint(problem, lp_X, beta_map)
|
|
140
|
+
_atom_type_constraint(problem, lp_X, G, H)
|
|
141
|
+
_edge_diff_constraint(problem, lp_X, lp_D, A_G, A_H)
|
|
142
|
+
_indicator_constraint(problem, lp_D, lp_G, lp_S, k)
|
|
143
|
+
|
|
144
|
+
status = problem.solve(lp.PULP_CBC_CMD(logPath=r"solver.log"))
|
|
145
|
+
|
|
146
|
+
np_X = np.zeros([m, m], dtype=np.int32)
|
|
147
|
+
for (i, j), v in lp_X.items():
|
|
148
|
+
np_X[i, j] = int(v.value())
|
|
149
|
+
|
|
150
|
+
return np_X, lp.LpStatus[status], lp_f.value()
|
aamutils/cmd/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from aamutils.cmd.expanding import configure_parser as configure_expanding_parser
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_argparser() -> argparse.ArgumentParser:
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
prog="AAM Utils",
|
|
9
|
+
description="A collection of atom-atom-mapping utility functions.",
|
|
10
|
+
)
|
|
11
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
12
|
+
|
|
13
|
+
configure_expanding_parser(subparsers)
|
|
14
|
+
|
|
15
|
+
return parser
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import rdkit.Chem.rdmolfiles as rdmolfiles
|
|
5
|
+
import rdkit.Chem.rdmolops as rdmolops
|
|
6
|
+
|
|
7
|
+
from utils import mol_to_graph, set_aam, graph_to_mol, is_valid_rxn_smiles
|
|
8
|
+
from algorithm.ilp import expand_partial_aam_balanced
|
|
9
|
+
from algorithm.aaming import get_its, get_rc, is_rc_valid
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("aamutils")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run(args):
|
|
15
|
+
outputfile = args.o
|
|
16
|
+
inputfile = args.inputfile
|
|
17
|
+
if outputfile is None:
|
|
18
|
+
in_path, in_ext = os.path.splitext(inputfile)
|
|
19
|
+
outputfile = "{}_extended.json".format(in_path)
|
|
20
|
+
rxn_smiles = []
|
|
21
|
+
with open(inputfile, "r") as f:
|
|
22
|
+
rxn_smiles = [line.strip() for line in f if is_valid_rxn_smiles(line)]
|
|
23
|
+
|
|
24
|
+
results = []
|
|
25
|
+
success_cnt = 0
|
|
26
|
+
for smiles in rxn_smiles:
|
|
27
|
+
result = {"input": smiles}
|
|
28
|
+
try:
|
|
29
|
+
r_smiles, p_smiles = smiles.split(">>")
|
|
30
|
+
r_mol = rdmolfiles.MolFromSmiles(r_smiles)
|
|
31
|
+
p_mol = rdmolfiles.MolFromSmiles(p_smiles)
|
|
32
|
+
r_mol = rdmolops.AddHs(r_mol, 1)
|
|
33
|
+
p_mol = rdmolops.AddHs(p_mol, 1)
|
|
34
|
+
G = mol_to_graph(r_mol)
|
|
35
|
+
H = mol_to_graph(p_mol)
|
|
36
|
+
|
|
37
|
+
M, status, e_diff = expand_partial_aam_balanced(G, H)
|
|
38
|
+
|
|
39
|
+
set_aam(G, H, M)
|
|
40
|
+
|
|
41
|
+
r_mol = graph_to_mol(G)
|
|
42
|
+
p_mol = graph_to_mol(H)
|
|
43
|
+
|
|
44
|
+
its = get_its(G, H)
|
|
45
|
+
rc = get_rc(its)
|
|
46
|
+
rc_invalid = not is_rc_valid(rc)
|
|
47
|
+
result_smiles = "{}>>{}".format(
|
|
48
|
+
rdmolfiles.MolToSmiles(r_mol), rdmolfiles.MolToSmiles(p_mol)
|
|
49
|
+
)
|
|
50
|
+
result["expanded_aam"] = result_smiles
|
|
51
|
+
result["ilp_status"] = status
|
|
52
|
+
result["optimization_result"] = e_diff
|
|
53
|
+
result["invalid_reaction_center"] = rc_invalid
|
|
54
|
+
result["reaction_edges"] = len(rc.nodes)
|
|
55
|
+
success_cnt += 1
|
|
56
|
+
except Exception as e:
|
|
57
|
+
result["error"] = str(e)
|
|
58
|
+
finally:
|
|
59
|
+
results.append(result)
|
|
60
|
+
|
|
61
|
+
with open(outputfile, "w") as f:
|
|
62
|
+
json.dump(results, f, indent=4)
|
|
63
|
+
logger.info(
|
|
64
|
+
"Expanded {} atom-atom-maps (failed: {})".format(
|
|
65
|
+
success_cnt, len(rxn_smiles) - success_cnt
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
logger.info("The result is written to file '{}'.".format(outputfile))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def configure_parser(subparsers):
|
|
72
|
+
parser = subparsers.add_parser(
|
|
73
|
+
"expand", help="Utility to expand a partial atom-atom-maps."
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"inputfile",
|
|
77
|
+
help=(
|
|
78
|
+
"A file containing reaction smiles with "
|
|
79
|
+
+ "partial atom-atom-maps. Reactions should be separated by newlines."
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument("-o", default=None, help="Path to output file.")
|
|
83
|
+
|
|
84
|
+
parser.set_defaults(func=run)
|
aamutils/utils.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import numpy as np
|
|
3
|
+
import networkx as nx
|
|
4
|
+
import rdkit.Chem as Chem
|
|
5
|
+
import rdkit.Chem.rdmolfiles as rdmolfiles
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def mol_to_graph(mol: Chem.rdchem.Mol) -> nx.Graph:
|
|
9
|
+
bond_order_map = {
|
|
10
|
+
"SINGLE": 1,
|
|
11
|
+
"DOUBLE": 2,
|
|
12
|
+
"TRIPLE": 3,
|
|
13
|
+
"QUADRUPLE": 4,
|
|
14
|
+
"AROMATIC": 1.5,
|
|
15
|
+
}
|
|
16
|
+
g = nx.Graph()
|
|
17
|
+
for atom in mol.GetAtoms():
|
|
18
|
+
aam = atom.GetAtomMapNum()
|
|
19
|
+
g.add_node(atom.GetIdx(), symbol=atom.GetSymbol(), aam=aam)
|
|
20
|
+
for bond in mol.GetBonds():
|
|
21
|
+
bond_type = str(bond.GetBondType()).split(".")[-1]
|
|
22
|
+
bond_order = 1
|
|
23
|
+
if bond_type in bond_order_map.keys():
|
|
24
|
+
bond_order = bond_order_map[bond_type]
|
|
25
|
+
g.add_edge(bond.GetBeginAtomIdx(), bond.GetEndAtomIdx(), bond=bond_order)
|
|
26
|
+
return g
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def graph_to_mol(
|
|
30
|
+
G: nx.Graph, symbol_key="symbol", aam_key="aam", bond_type_key="bond"
|
|
31
|
+
) -> Chem.rdchem.Mol:
|
|
32
|
+
bond_order_map = {
|
|
33
|
+
1: Chem.rdchem.BondType.SINGLE,
|
|
34
|
+
2: Chem.rdchem.BondType.DOUBLE,
|
|
35
|
+
3: Chem.rdchem.BondType.TRIPLE,
|
|
36
|
+
4: Chem.rdchem.BondType.QUADRUPLE,
|
|
37
|
+
1.5: Chem.rdchem.BondType.AROMATIC,
|
|
38
|
+
}
|
|
39
|
+
rw_mol = Chem.rdchem.RWMol()
|
|
40
|
+
for n, d in G.nodes(data=True):
|
|
41
|
+
idx = rw_mol.AddAtom(Chem.rdchem.Atom(d[symbol_key]))
|
|
42
|
+
if aam_key in d.keys() and d[aam_key] >= 0:
|
|
43
|
+
rw_mol.GetAtomWithIdx(idx).SetAtomMapNum(d[aam_key])
|
|
44
|
+
assert n == idx
|
|
45
|
+
for n1, n2, d in G.edges(data=True):
|
|
46
|
+
rw_mol.AddBond(n1, n2, bond_order_map[d[bond_type_key]])
|
|
47
|
+
return rw_mol.GetMol()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_beta_map(G, H, aam_key="aam"):
|
|
51
|
+
node2aam_G = collections.defaultdict(
|
|
52
|
+
lambda: -1, {n: d[aam_key] for (n, d) in G.nodes(data=True) if d[aam_key] > 0}
|
|
53
|
+
)
|
|
54
|
+
aam2node_H = collections.defaultdict(
|
|
55
|
+
lambda: -1, {d[aam_key]: n for (n, d) in H.nodes(data=True) if d[aam_key] > 0}
|
|
56
|
+
)
|
|
57
|
+
beta_map = []
|
|
58
|
+
for n_G, aam_G in node2aam_G.items():
|
|
59
|
+
if aam_G in aam2node_H and aam2node_H[aam_G] > -1:
|
|
60
|
+
beta_map.append((n_G, aam2node_H[aam_G], aam_G))
|
|
61
|
+
return beta_map
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def set_aam(G, H, M, beta_map=None, aam_key="aam"):
|
|
65
|
+
if beta_map is None:
|
|
66
|
+
beta_map = get_beta_map(G, H)
|
|
67
|
+
used_atom_numbers = [aam for _, _, aam in beta_map]
|
|
68
|
+
|
|
69
|
+
aam_G = collections.defaultdict(lambda: -1)
|
|
70
|
+
for bi, _, aam in beta_map:
|
|
71
|
+
aam_G[bi] = aam
|
|
72
|
+
|
|
73
|
+
next_aam_nr = 1
|
|
74
|
+
for n, d in G.nodes(data=True):
|
|
75
|
+
if aam_G[n] > -1:
|
|
76
|
+
aam_nr = aam_G[n]
|
|
77
|
+
else:
|
|
78
|
+
while next_aam_nr in used_atom_numbers:
|
|
79
|
+
next_aam_nr += 1
|
|
80
|
+
aam_nr = next_aam_nr
|
|
81
|
+
used_atom_numbers.append(aam_nr)
|
|
82
|
+
d[aam_key] = int(aam_nr)
|
|
83
|
+
aam_G[n] = aam_nr
|
|
84
|
+
|
|
85
|
+
aam_G = np.array([v for _, v in sorted(aam_G.items(), key=lambda x: x[0])])
|
|
86
|
+
for (_, d), aam in zip(H.nodes(data=True), np.dot(aam_G.T, M)):
|
|
87
|
+
d[aam_key] = int(aam)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def is_valid_rxn_smiles(smiles):
|
|
91
|
+
smiles_token = smiles.split(">>")
|
|
92
|
+
if len(smiles_token) != 2:
|
|
93
|
+
return False
|
|
94
|
+
mol1 = rdmolfiles.MolFromSmiles(smiles_token[0])
|
|
95
|
+
if mol1 is None:
|
|
96
|
+
return False
|
|
97
|
+
mol2 = rdmolfiles.MolFromSmiles(smiles_token[0])
|
|
98
|
+
if mol2 is None:
|
|
99
|
+
return False
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def print_graph(graph):
|
|
104
|
+
print(
|
|
105
|
+
"Graph Nodes: {}".format(
|
|
106
|
+
" ".join(
|
|
107
|
+
[
|
|
108
|
+
"[{}]{}:{}".format(
|
|
109
|
+
n[0], n[1].get("symbol", None), n[1].get("aam", None)
|
|
110
|
+
)
|
|
111
|
+
for n in graph.nodes(data=True)
|
|
112
|
+
]
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
print(
|
|
117
|
+
"Graph Edges: {}".format(
|
|
118
|
+
" ".join(
|
|
119
|
+
[
|
|
120
|
+
"[{}]-[{}]:{}".format(n[0], n[1], n[2]["bond"])
|
|
121
|
+
for n in graph.edges(data=True)
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: aamutils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Collection of atom-atom-mapping utility functions.
|
|
5
|
+
Project-URL: homepage, https://github.com/klausweinbauer/AAMUtils
|
|
6
|
+
Project-URL: source, https://github.com/klausweinbauer/AAMUtils
|
|
7
|
+
Project-URL: issues, https://github.com/klausweinbauer/AAMUtils/issues
|
|
8
|
+
Author-email: Klaus Weinbauer <klaus@bioinf.uni-leipzig.de>
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: fgutils<0.1.0,>=0.0.17
|
|
15
|
+
Requires-Dist: networkx==3.3
|
|
16
|
+
Requires-Dist: numpy<2.0.0,>=1.26.0
|
|
17
|
+
Requires-Dist: pillow==10.3.0
|
|
18
|
+
Requires-Dist: pulp==2.8.0
|
|
19
|
+
Requires-Dist: rdkit==2024.3.1
|
|
20
|
+
Requires-Dist: scipy==1.14.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Atom-atom-mapping Utils
|
|
24
|
+
|
|
25
|
+
A collection of atom-atom-mapping utility functions.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
TODO
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
The input is a list of partial atom-atom-maps (AAMs). Data is read line-by-line
|
|
34
|
+
from a text file. Each line should contain one reaction SMILES.
|
|
35
|
+
|
|
36
|
+
Here is a simple example extending the partial AAM to a complete AAM. First
|
|
37
|
+
generate the input data:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
echo "CCC[Cl:1].[N:2]>>CCC[N:2].[Cl:1]" > testinput.txt
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Next, run AAMUtils to expand the partial AAM.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 ./aamutils/__main__.py expand testinput.txt
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The output is written to 'testinput_extended.json'.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cat testinput_extended.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
[
|
|
57
|
+
{
|
|
58
|
+
"input": "CCC[Cl:1].[N:2]>>CCC[N:2].[Cl:1]",
|
|
59
|
+
"expanded_aam": "[Cl:1][CH2:5][CH2:4][CH3:3].[NH3:2]>>[ClH:1].[NH2:2][CH2:5][CH2:4][CH3:3]",
|
|
60
|
+
"ilp_status": "Optimal",
|
|
61
|
+
"optimization_result": 2.0,
|
|
62
|
+
"invalid_reaction_center": false,
|
|
63
|
+
"reaction_edges": 3
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Functionality
|
|
69
|
+
Here is an overview of implemented functionallity:
|
|
70
|
+
|
|
71
|
+
- SMILES to graph and graph to SMILES parsing
|
|
72
|
+
- Reaction center validity checks
|
|
73
|
+
- ITS graph generation
|
|
74
|
+
- Expand partial AAM to complete AAM on balanced reactions
|
|
75
|
+
- AAMing based on minimal chemical distance (MCD) for balanced reactions
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
This project is licensed under MIT License - see the [License](LICENSE) file
|
|
80
|
+
for details.
|
|
81
|
+
|
|
82
|
+
## Acknowledgments
|
|
83
|
+
|
|
84
|
+
This project has received funding from the European Unions Horizon Europe
|
|
85
|
+
Doctoral Network programme under the Marie-Skłodowska-Curie grant agreement No
|
|
86
|
+
101072930 (TACsy -- Training Alliance for Computational)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
aamutils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
aamutils/__main__.py,sha256=DasuV1GXyLtaaps146UZDGCUup3xkQMi1cx-FWX54w0,485
|
|
3
|
+
aamutils/utils.py,sha256=d-9xXpHrEwY6-bnk_uOuoPp6h9BiBOlJH78gAuiaMbM,3794
|
|
4
|
+
aamutils/algorithm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
aamutils/algorithm/aaming.py,sha256=nz179yZ6gdhTCVRwZk4LuaGWzz9m8G58IDZuYEQ4YfI,4472
|
|
6
|
+
aamutils/algorithm/ilp.py,sha256=__XejyI1l93d0h8YEDQ1P2xNBEnOArWtIqp57STwNCE,4707
|
|
7
|
+
aamutils/cmd/__init__.py,sha256=v_hXGEsutZQjDS2rK7Q8SSF6qgObwNghoiQW-kT9zwg,415
|
|
8
|
+
aamutils/cmd/expanding.py,sha256=h-Nzwo_xGb9q6YLKXT9Sxb_IUEJnUd59jS--IWe25DA,2717
|
|
9
|
+
aamutils-0.0.1.dist-info/METADATA,sha256=rzUe5A_mkwpmttKFeWPyoP3sLNkg3Qfr96POJdINTDI,2430
|
|
10
|
+
aamutils-0.0.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
11
|
+
aamutils-0.0.1.dist-info/licenses/LICENSE,sha256=_oKtgkrP1wX2OJZmh5jhsJu5aWm79813yq0wxYKh0I4,1062
|
|
12
|
+
aamutils-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Klaus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|