aamutils 0.0.3__tar.gz → 0.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {aamutils-0.0.3 → aamutils-0.0.5}/PKG-INFO +5 -5
  2. {aamutils-0.0.3 → aamutils-0.0.5}/README.md +4 -4
  3. aamutils-0.0.5/aamutils/__init__.py +1 -0
  4. {aamutils-0.0.3 → aamutils-0.0.5}/aamutils/algorithm/ilp.py +47 -2
  5. aamutils-0.0.5/aamutils/mapping.py +55 -0
  6. {aamutils-0.0.3 → aamutils-0.0.5}/aamutils/utils.py +64 -2
  7. {aamutils-0.0.3 → aamutils-0.0.5}/pyproject.toml +1 -1
  8. {aamutils-0.0.3 → aamutils-0.0.5}/test/algorithm/test_ilp.py +1 -25
  9. aamutils-0.0.5/test/test_mapping.py +16 -0
  10. aamutils-0.0.3/test/algorithm/__init__.py +0 -0
  11. {aamutils-0.0.3 → aamutils-0.0.5}/.github/workflows/publish-package.yml +0 -0
  12. {aamutils-0.0.3 → aamutils-0.0.5}/.github/workflows/test-and-lint.yml +0 -0
  13. {aamutils-0.0.3 → aamutils-0.0.5}/.gitignore +0 -0
  14. {aamutils-0.0.3 → aamutils-0.0.5}/LICENSE +0 -0
  15. {aamutils-0.0.3 → aamutils-0.0.5}/Makefile +0 -0
  16. {aamutils-0.0.3 → aamutils-0.0.5}/aamutils/__main__.py +0 -0
  17. {aamutils-0.0.3/aamutils → aamutils-0.0.5/aamutils/algorithm}/__init__.py +0 -0
  18. {aamutils-0.0.3 → aamutils-0.0.5}/aamutils/algorithm/aaming.py +0 -0
  19. {aamutils-0.0.3 → aamutils-0.0.5}/aamutils/cmd/__init__.py +0 -0
  20. {aamutils-0.0.3 → aamutils-0.0.5}/aamutils/cmd/expanding.py +0 -0
  21. {aamutils-0.0.3 → aamutils-0.0.5}/benchmark.py +0 -0
  22. {aamutils-0.0.3 → aamutils-0.0.5}/conf.py +0 -0
  23. {aamutils-0.0.3 → aamutils-0.0.5}/data/test_dataset.json +0 -0
  24. {aamutils-0.0.3 → aamutils-0.0.5}/doc/getting_started.rst +0 -0
  25. {aamutils-0.0.3 → aamutils-0.0.5}/doc/reference.rst +0 -0
  26. {aamutils-0.0.3 → aamutils-0.0.5}/index.rst +0 -0
  27. {aamutils-0.0.3 → aamutils-0.0.5}/lint.sh +0 -0
  28. {aamutils-0.0.3 → aamutils-0.0.5}/make.bat +0 -0
  29. {aamutils-0.0.3 → aamutils-0.0.5}/requirements.txt +0 -0
  30. {aamutils-0.0.3/aamutils/algorithm → aamutils-0.0.5/test}/__init__.py +0 -0
  31. {aamutils-0.0.3/test → aamutils-0.0.5/test/algorithm}/__init__.py +0 -0
  32. {aamutils-0.0.3 → aamutils-0.0.5}/test/algorithm/test_aaming.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aamutils
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Collection of atom-atom-mapping utility functions.
5
5
  Project-URL: homepage, https://github.com/klausweinbauer/AAMUtils
6
6
  Project-URL: source, https://github.com/klausweinbauer/AAMUtils
@@ -78,18 +78,18 @@ reported results can be reproduced by running the following commands:
78
78
  ### (1) Extend partial reaction center (50%, 75% and 100% missing atoms)
79
79
 
80
80
  ```
81
- python3 benchmark.py --remove-mode rc --remove-ratio 0.5
81
+ python3 benchmark.py --remove-mode rc --remove-ratio 0.5 --seed 42
82
82
  ```
83
83
  ```
84
- python3 benchmark.py --remove-mode rc --remove-ratio 0.75
84
+ python3 benchmark.py --remove-mode rc --remove-ratio 0.75 --seed 42
85
85
  ```
86
86
  ```
87
- python3 benchmark.py --remove-mode rc --remove-ratio 1
87
+ python3 benchmark.py --remove-mode rc --remove-ratio 1 --seed 42
88
88
  ```
89
89
 
90
90
  ### (2) Extend partial AAM with fully mapped RC
91
91
  ```
92
- python3 benchmark.py --remove-mode keep_rc --remove-ratio 1
92
+ python3 benchmark.py --remove-mode keep_rc --remove-ratio 1 --seed 42
93
93
  ```
94
94
 
95
95
 
@@ -56,18 +56,18 @@ reported results can be reproduced by running the following commands:
56
56
  ### (1) Extend partial reaction center (50%, 75% and 100% missing atoms)
57
57
 
58
58
  ```
59
- python3 benchmark.py --remove-mode rc --remove-ratio 0.5
59
+ python3 benchmark.py --remove-mode rc --remove-ratio 0.5 --seed 42
60
60
  ```
61
61
  ```
62
- python3 benchmark.py --remove-mode rc --remove-ratio 0.75
62
+ python3 benchmark.py --remove-mode rc --remove-ratio 0.75 --seed 42
63
63
  ```
64
64
  ```
65
- python3 benchmark.py --remove-mode rc --remove-ratio 1
65
+ python3 benchmark.py --remove-mode rc --remove-ratio 1 --seed 42
66
66
  ```
67
67
 
68
68
  ### (2) Extend partial AAM with fully mapped RC
69
69
  ```
70
- python3 benchmark.py --remove-mode keep_rc --remove-ratio 1
70
+ python3 benchmark.py --remove-mode keep_rc --remove-ratio 1 --seed 42
71
71
  ```
72
72
 
73
73
 
@@ -0,0 +1 @@
1
+ from .mapping import Mapper
@@ -57,19 +57,48 @@ def _indicator_constraint(problem, D, G, S, k):
57
57
  for j in range(m):
58
58
  problem += (
59
59
  D[i, j] <= k * G[i, j],
60
- "g_indicator_constraint_lower_{}-{}".format(i, j),
60
+ "g_indicator_constraint1_{}-{}".format(i, j),
61
+ )
62
+ problem += (
63
+ D[i, j] >= -k + G[i, j] * (k + 1),
64
+ "g_indicator_constraint2_{}-{}".format(i, j),
61
65
  )
62
66
  problem += (
63
67
  -D[i, j] <= k * S[i, j],
64
- "s_indicator_constraint_lower_{}-{}".format(i, j),
68
+ "s_indicator_constraint1_{}-{}".format(i, j),
69
+ )
70
+ problem += (
71
+ -D[i, j] >= -k + S[i, j] * (k + 1),
72
+ "s_indicator_constraint2_{}-{}".format(i, j),
65
73
  )
66
74
 
67
75
 
76
+ def _expected_rc_atoms_constraint(problem, G, S, expected_rc_atoms):
77
+ m = int(np.sqrt(len(G.keys())))
78
+ assert m == int(np.sqrt(len(S.keys())))
79
+
80
+ lp_io = lp.LpVariable.dicts("io", [i for i in range(m)], cat=lp.LpInteger)
81
+ lp_rc = lp.LpVariable.dicts("rc", [i for i in range(m)], cat=lp.LpBinary)
82
+ for i in range(m):
83
+ problem += (
84
+ lp_io[i] == lp.lpSum([G[i, j] + S[i, j] for j in range(m)]),
85
+ "idx_io_sum_{}".format(i),
86
+ )
87
+ problem += (lp_io[i] <= m * lp_rc[i], "is_rc_indicator1_{}".format(i))
88
+ problem += (lp_io[i] >= lp_rc[i], "is_rc_indicator_{}".format(i))
89
+
90
+ problem += (
91
+ expected_rc_atoms == lp.lpSum([lp_rc[i] for i in range(m)]),
92
+ "rc_atom_constraint",
93
+ )
94
+
95
+
68
96
  def expand_partial_aam_balanced(
69
97
  G: nx.Graph,
70
98
  H: nx.Graph,
71
99
  beta_map: None | list[tuple[int, int, int]] = None,
72
100
  bond_key="bond",
101
+ expected_rc: None | tuple[None | int, None | int] = None,
73
102
  ) -> tuple[np.ndarray, str, float]:
74
103
  """Function to extend the partial atom-atom map beta of the balanced
75
104
  reaction G \u2192 H to a full atom-atom map based on the Minimal Chemical
@@ -81,6 +110,14 @@ def expand_partial_aam_balanced(
81
110
  specified the existing mapping in G and H will be used.
82
111
  :param bond_key: (optional) The edge label in G and H which encodes the
83
112
  bond order.
113
+ :param expected_rc: (optional) Configuration for the expected reaction
114
+ center. The first parameter in the tuple is the number of atoms and
115
+ the second parameter is the number of bonds. The atoms and bonds are
116
+ only constraint if the value is not None. It is possible to constraint
117
+ only atoms or only bonds. Constraining the expected reaction center can
118
+ help to get correct AAM for reactions where the Minimal Chemical
119
+ Distance is unsuitable, e.g. for a Diels-Alder reaction you can set
120
+ expected_rc to (6, 6).
84
121
 
85
122
  :returns: A 3-tuple. The first element is the mapping matrix X. The second
86
123
  element is the status string of the ILP solver (e.g. 'Optimal') and the
@@ -134,6 +171,14 @@ def expand_partial_aam_balanced(
134
171
  lp_f == lp.lpSum([lp_G[i, j] + lp_S[i, j] for i in range(m) for j in range(m)]),
135
172
  "objective",
136
173
  )
174
+ if expected_rc is not None:
175
+ if not isinstance(expected_rc, tuple) or len(expected_rc) != 2:
176
+ raise ValueError("Argument expected_rc must be a tuple of size 2.")
177
+ expected_rc_atoms, expected_rc_bonds = expected_rc
178
+ if expected_rc_bonds is not None:
179
+ problem += (lp_f == 2 * expected_rc_bonds, "rc_bond_constraint")
180
+ if expected_rc_atoms is not None:
181
+ _expected_rc_atoms_constraint(problem, lp_G, lp_S, expected_rc_atoms)
137
182
 
138
183
  _bijection_constraint(problem, lp_X)
139
184
  _beta_constraint(problem, lp_X, beta_map)
@@ -0,0 +1,55 @@
1
+ import rdkit.Chem.rdmolops as rdmolops
2
+ import rdkit.Chem.rdmolfiles as rdmolfiles
3
+
4
+ from aamutils.utils import graph_to_mol, mol_to_graph, set_aam
5
+ from aamutils.algorithm.ilp import expand_partial_aam_balanced
6
+
7
+
8
+ class Mapper:
9
+ def __init__(self, include_H=False):
10
+ self.include_H = include_H
11
+
12
+ def _map_rxn(self, rxn_smiles: str, expected_rc):
13
+ g_smiles, h_smiles = rxn_smiles.split(">>")
14
+ g_mol = rdmolfiles.MolFromSmiles(g_smiles)
15
+ h_mol = rdmolfiles.MolFromSmiles(h_smiles)
16
+ if self.include_H:
17
+ g_mol = rdmolops.AddHs(g_mol, 0)
18
+ h_mol = rdmolops.AddHs(h_mol, 0)
19
+ G = mol_to_graph(g_mol)
20
+ H = mol_to_graph(h_mol)
21
+
22
+ M, status, e_diff = expand_partial_aam_balanced(G, H, expected_rc=expected_rc)
23
+
24
+ set_aam(G, H, M)
25
+
26
+ g_mol = graph_to_mol(G)
27
+ h_mol = graph_to_mol(H)
28
+
29
+ aam_smiles = "{}>>{}".format(
30
+ rdmolfiles.MolToSmiles(g_mol), rdmolfiles.MolToSmiles(h_mol)
31
+ )
32
+ return aam_smiles
33
+
34
+ def get_aam(self, smiles: str | list[str], expected_rc) -> str | list[str]:
35
+ return_list = True
36
+ if isinstance(smiles, str):
37
+ smiles = [smiles]
38
+ return_list = False
39
+ if expected_rc is None:
40
+ expected_rc = [None for _ in range(len(smiles))]
41
+ elif not isinstance(expected_rc, list):
42
+ expected_rc = [expected_rc]
43
+ if len(smiles) != len(expected_rc):
44
+ raise ValueError(
45
+ "Argument min_bonds must be of same length ({}) as smiles.".format(
46
+ len(smiles)
47
+ )
48
+ )
49
+ aam_smiles = []
50
+ print(expected_rc)
51
+ for smiles, exp_rc in zip(smiles, expected_rc):
52
+ aam_smiles.append(self._map_rxn(smiles, expected_rc=exp_rc))
53
+ if not return_list:
54
+ return aam_smiles[0]
55
+ return aam_smiles
@@ -3,6 +3,7 @@ import numpy as np
3
3
  import networkx as nx
4
4
  import rdkit.Chem as Chem
5
5
  import rdkit.Chem.rdmolfiles as rdmolfiles
6
+ import rdkit.Chem.rdDepictor as rdDepictor
6
7
 
7
8
 
8
9
  def mol_to_graph(mol: Chem.rdchem.Mol) -> nx.Graph:
@@ -26,6 +27,16 @@ def mol_to_graph(mol: Chem.rdchem.Mol) -> nx.Graph:
26
27
  return g
27
28
 
28
29
 
30
+ def smiles_to_graph(smiles: str) -> nx.Graph | tuple[nx.Graph, nx.Graph]:
31
+ if ">>" in smiles:
32
+ smiles_token = smiles.split(">>")
33
+ g = mol_to_graph(rdmolfiles.MolFromSmiles(smiles_token[0]))
34
+ h = mol_to_graph(rdmolfiles.MolFromSmiles(smiles_token[1]))
35
+ return g, h
36
+ else:
37
+ return mol_to_graph(rdmolfiles.MolFromSmiles(smiles))
38
+
39
+
29
40
  def graph_to_mol(
30
41
  G: nx.Graph, symbol_key="symbol", aam_key="aam", bond_type_key="bond"
31
42
  ) -> Chem.rdchem.Mol:
@@ -37,13 +48,16 @@ def graph_to_mol(
37
48
  1.5: Chem.rdchem.BondType.AROMATIC,
38
49
  }
39
50
  rw_mol = Chem.rdchem.RWMol()
51
+ idx_map = {}
40
52
  for n, d in G.nodes(data=True):
41
53
  idx = rw_mol.AddAtom(Chem.rdchem.Atom(d[symbol_key]))
54
+ idx_map[n] = idx
42
55
  if aam_key in d.keys() and d[aam_key] >= 0:
43
56
  rw_mol.GetAtomWithIdx(idx).SetAtomMapNum(d[aam_key])
44
- assert n == idx
45
57
  for n1, n2, d in G.edges(data=True):
46
- rw_mol.AddBond(n1, n2, bond_order_map[d[bond_type_key]])
58
+ idx1 = idx_map[n1]
59
+ idx2 = idx_map[n2]
60
+ rw_mol.AddBond(idx1, idx2, bond_order_map[d[bond_type_key]])
47
61
  return rw_mol.GetMol()
48
62
 
49
63
 
@@ -123,3 +137,51 @@ def print_graph(graph):
123
137
  )
124
138
  )
125
139
  )
140
+
141
+
142
+ def its2mol(its: nx.Graph, aam_key="aam", bond_key="bond") -> Chem.rdchem.Mol:
143
+ _its = its.copy()
144
+ for n in _its.nodes:
145
+ _its.nodes[n][aam_key] = n
146
+ for u, v in _its.edges():
147
+ _its[u][v][bond_key] = 1
148
+ return graph_to_mol(_its)
149
+
150
+
151
+ def plot_its(
152
+ its, ax, bond_key="bond", aam_key="aam", symbol_key="symbol", use_mol_coords=True
153
+ ):
154
+ bond_char = {None: "∅", 1: "—", 2: "=", 3: "≡"}
155
+ mol = its2mol(its, aam_key=aam_key, bond_key=bond_key)
156
+
157
+ if use_mol_coords:
158
+ positions = {}
159
+ conformer = rdDepictor.Compute2DCoords(mol)
160
+ for i, atom in enumerate(mol.GetAtoms()):
161
+ aam = atom.GetAtomMapNum()
162
+ apos = mol.GetConformer(conformer).GetAtomPosition(i)
163
+ positions[aam] = [apos.x, apos.y]
164
+ else:
165
+ positions = nx.spring_layout(its)
166
+
167
+ ax.axis("equal")
168
+ ax.axis("off")
169
+
170
+ nx.draw_networkx_edges(its, positions, edge_color="#000000", ax=ax)
171
+ nx.draw_networkx_nodes(its, positions, node_color="#FFFFFF", node_size=500, ax=ax)
172
+
173
+ labels = {n: "{}:{}".format(d[symbol_key], n) for n, d in its.nodes(data=True)}
174
+ edge_labels = {}
175
+ for u, v, d in its.edges(data=True):
176
+ bc1 = d[bond_key][0]
177
+ bc2 = d[bond_key][1]
178
+ if bc1 == bc2:
179
+ continue
180
+ if bc1 in bond_char.keys():
181
+ bc1 = bond_char[bc1]
182
+ if bc2 in bond_char.keys():
183
+ bc2 = bond_char[bc2]
184
+ edge_labels[(u, v)] = "({},{})".format(bc1, bc2)
185
+
186
+ nx.draw_networkx_labels(its, positions, labels=labels, ax=ax)
187
+ nx.draw_networkx_edge_labels(its, positions, edge_labels=edge_labels, ax=ax)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "aamutils"
7
- version = "0.0.3"
7
+ version = "0.0.5"
8
8
  authors = [
9
9
  {name="Klaus Weinbauer", email="klaus@bioinf.uni-leipzig.de"}
10
10
  ]
@@ -34,26 +34,6 @@ def test_example1():
34
34
  np.testing.assert_array_equal(X, exp_X)
35
35
 
36
36
 
37
- def test_example1_without_aam():
38
- # The tested mapping is not correct. This is to show equal solutions based
39
- # on the implemented optimization criterion. The correct mapping is in
40
- # test_example1()
41
- mol_G = rdmolfiles.MolFromSmiles("CCC[Cl].[N]")
42
- mol_H = rdmolfiles.MolFromSmiles("CCC[N].[Cl]")
43
- exp_X = np.zeros((5, 5))
44
- exp_X[0, 2] = 1
45
- exp_X[1, 1] = 1
46
- exp_X[2, 0] = 1
47
- exp_X[3, 4] = 1
48
- exp_X[4, 3] = 1
49
- G = mol_to_graph(mol_G)
50
- H = mol_to_graph(mol_H)
51
- X, status, v = expand_partial_aam_balanced(G, H)
52
- assert 4 == v
53
- assert "Optimal" == status
54
- np.testing.assert_array_equal(X, exp_X)
55
-
56
-
57
37
  def test_example2_m1():
58
38
  mol_G = rdmolfiles.MolFromSmiles("[C:1][C:2]=[C:3][C:4]")
59
39
  mol_H = rdmolfiles.MolFromSmiles("[C:1].[C:2]=[C:4][C:3]")
@@ -73,11 +53,7 @@ def test_example2_m1():
73
53
  def test_example2_m2():
74
54
  mol_G = rdmolfiles.MolFromSmiles("CC=CC")
75
55
  mol_H = rdmolfiles.MolFromSmiles("C.C=CC")
76
- exp_X = np.zeros((4, 4))
77
- exp_X[0, 3] = 1
78
- exp_X[1, 2] = 1
79
- exp_X[2, 1] = 1
80
- exp_X[3, 0] = 1
56
+ exp_X = np.eye(4)
81
57
  G = mol_to_graph(mol_G)
82
58
  H = mol_to_graph(mol_H)
83
59
  X, status, v = expand_partial_aam_balanced(G, H)
@@ -0,0 +1,16 @@
1
+ from aamutils.mapping import Mapper
2
+ from aamutils.algorithm.aaming import get_its, get_rc
3
+ from aamutils.utils import smiles_to_graph
4
+
5
+
6
+ def test_mapper_with_expected_rc():
7
+ # Diels-Alder reaction example
8
+ smiles = "C1=CC=CC1.C1=CC=CC1>>C1=CC2CC1C3C2C=CC3"
9
+ mapper = Mapper()
10
+ aam_smiles = mapper.get_aam(smiles, expected_rc=(6, 6))
11
+ assert isinstance(aam_smiles, str)
12
+ g, h = smiles_to_graph(aam_smiles)
13
+ its = get_its(g, h)
14
+ rc = get_rc(its)
15
+ assert len(rc.nodes) == 6
16
+ assert len(rc.edges) == 6
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes