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 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()
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.