chython 2.19__cp313-cp313-win_amd64.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.
Files changed (175) hide show
  1. chython/__init__.py +35 -0
  2. chython/_functions.py +95 -0
  3. chython/_java.py +57 -0
  4. chython/algorithms/__init__.py +18 -0
  5. chython/algorithms/_isomorphism.cp313-win_amd64.pyd +0 -0
  6. chython/algorithms/_isomorphism.pyx +189 -0
  7. chython/algorithms/_rings.cp313-win_amd64.pyd +0 -0
  8. chython/algorithms/_rings.pyx +478 -0
  9. chython/algorithms/aromatics/__init__.py +27 -0
  10. chython/algorithms/aromatics/_rules.py +124 -0
  11. chython/algorithms/aromatics/kekule.py +532 -0
  12. chython/algorithms/aromatics/test/__init__.py +18 -0
  13. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  14. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  15. chython/algorithms/aromatics/thiele.py +222 -0
  16. chython/algorithms/calculate2d/__init__.py +23 -0
  17. chython/algorithms/calculate2d/clean2d.js +1 -0
  18. chython/algorithms/calculate2d/molecule.py +191 -0
  19. chython/algorithms/calculate2d/reaction.py +75 -0
  20. chython/algorithms/conformers.py +106 -0
  21. chython/algorithms/depict.py +596 -0
  22. chython/algorithms/fingerprints/__init__.py +43 -0
  23. chython/algorithms/fingerprints/linear.py +202 -0
  24. chython/algorithms/fingerprints/morgan.py +132 -0
  25. chython/algorithms/fingerprints/test/__init__.py +18 -0
  26. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  27. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  28. chython/algorithms/groups/__init__.py +212 -0
  29. chython/algorithms/groups/_functional.py +290 -0
  30. chython/algorithms/groups/_oxidations.py +70 -0
  31. chython/algorithms/groups/_protective.py +964 -0
  32. chython/algorithms/groups/_reactions.py +809 -0
  33. chython/algorithms/groups/_reductions.py +88 -0
  34. chython/algorithms/groups/_transformations.py +278 -0
  35. chython/algorithms/groups/test/__init__.py +18 -0
  36. chython/algorithms/groups/test/test_deprotection.py +108 -0
  37. chython/algorithms/groups/test/test_reactions.py +377 -0
  38. chython/algorithms/isomorphism.py +664 -0
  39. chython/algorithms/mapping/__init__.py +188 -0
  40. chython/algorithms/mapping/_groups.py +94 -0
  41. chython/algorithms/mapping/_reactions.py +72 -0
  42. chython/algorithms/mapping/attention.py +266 -0
  43. chython/algorithms/mapping/reconstruct.py +346 -0
  44. chython/algorithms/mcs.py +201 -0
  45. chython/algorithms/morgan.py +77 -0
  46. chython/algorithms/rings.py +178 -0
  47. chython/algorithms/smiles.py +597 -0
  48. chython/algorithms/standardize/__init__.py +30 -0
  49. chython/algorithms/standardize/_groups.py +958 -0
  50. chython/algorithms/standardize/_isomers.py +330 -0
  51. chython/algorithms/standardize/_metal_organics.py +189 -0
  52. chython/algorithms/standardize/_reagents.py +44 -0
  53. chython/algorithms/standardize/_salts.py +68 -0
  54. chython/algorithms/standardize/molecule.py +475 -0
  55. chython/algorithms/standardize/reaction.py +384 -0
  56. chython/algorithms/standardize/resonance.py +191 -0
  57. chython/algorithms/standardize/salts.py +128 -0
  58. chython/algorithms/standardize/saturation.py +373 -0
  59. chython/algorithms/standardize/test/__init__.py +18 -0
  60. chython/algorithms/standardize/test/test_canonicalize.py +66 -0
  61. chython/algorithms/standardize/test/test_groups.py +146 -0
  62. chython/algorithms/standardize/test/test_isomers.py +198 -0
  63. chython/algorithms/stereo.py +1215 -0
  64. chython/algorithms/tautomers/__init__.py +167 -0
  65. chython/algorithms/tautomers/_acid.py +59 -0
  66. chython/algorithms/tautomers/_base.py +116 -0
  67. chython/algorithms/tautomers/_keto_enol.py +107 -0
  68. chython/algorithms/tautomers/acid_base.py +208 -0
  69. chython/algorithms/tautomers/heteroarenes.py +103 -0
  70. chython/algorithms/tautomers/keto_enol.py +143 -0
  71. chython/algorithms/tautomers/test/__init__.py +18 -0
  72. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  73. chython/algorithms/test/__init__.py +18 -0
  74. chython/algorithms/test/test_isomorphism.py +58 -0
  75. chython/algorithms/test/test_smiles.py +76 -0
  76. chython/algorithms/x3dom.py +350 -0
  77. chython/containers/__init__.py +48 -0
  78. chython/containers/_pack_v2.cp313-win_amd64.pyd +0 -0
  79. chython/containers/_pack_v2.pyx +271 -0
  80. chython/containers/_unpack_v0v2.cp313-win_amd64.pyd +0 -0
  81. chython/containers/_unpack_v0v2.pyx +302 -0
  82. chython/containers/bonds.py +255 -0
  83. chython/containers/cgr.py +115 -0
  84. chython/containers/chimera.py +63 -0
  85. chython/containers/graph.py +199 -0
  86. chython/containers/molecule.py +817 -0
  87. chython/containers/query.py +71 -0
  88. chython/containers/rdkit.py +183 -0
  89. chython/containers/reaction.py +324 -0
  90. chython/containers/test/__init__.py +18 -0
  91. chython/containers/test/test_molecule.py +117 -0
  92. chython/containers/test/test_pack.py +60 -0
  93. chython/containers/test/test_rdkit.py +70 -0
  94. chython/containers/test/test_reaction.py +40 -0
  95. chython/exceptions.py +114 -0
  96. chython/files/MRVrw.py +532 -0
  97. chython/files/PDBrw.py +255 -0
  98. chython/files/RDFrw.py +309 -0
  99. chython/files/SDFrw.py +221 -0
  100. chython/files/__init__.py +30 -0
  101. chython/files/_convert.py +207 -0
  102. chython/files/_mapping.py +117 -0
  103. chython/files/_xyz.cp313-win_amd64.pyd +0 -0
  104. chython/files/_xyz.pyx +74 -0
  105. chython/files/daylight/__init__.py +24 -0
  106. chython/files/daylight/parser.py +157 -0
  107. chython/files/daylight/smarts.py +107 -0
  108. chython/files/daylight/smiles.py +260 -0
  109. chython/files/daylight/test/__init__.py +18 -0
  110. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  111. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  112. chython/files/daylight/test/test_parser.py +118 -0
  113. chython/files/daylight/test/test_tokenize.py +77 -0
  114. chython/files/daylight/tokenize.py +376 -0
  115. chython/files/libinchi/__init__.py +22 -0
  116. chython/files/libinchi/libinchi.dll +0 -0
  117. chython/files/libinchi/wrapper.py +554 -0
  118. chython/files/mdl/__init__.py +25 -0
  119. chython/files/mdl/emol.py +251 -0
  120. chython/files/mdl/erxn.py +67 -0
  121. chython/files/mdl/mol.py +186 -0
  122. chython/files/mdl/read.py +223 -0
  123. chython/files/mdl/rxn.py +67 -0
  124. chython/files/mdl/stereo.py +63 -0
  125. chython/files/mdl/write.py +201 -0
  126. chython/files/opsin.py +35 -0
  127. chython/files/test/__init__.py +0 -0
  128. chython/files/test/test_inchi.py +27 -0
  129. chython/files/test/test_mrv.py +47 -0
  130. chython/files/test/test_sdf_rdf.py +64 -0
  131. chython/files/test/test_xyz.py +20 -0
  132. chython/files/xyz.py +78 -0
  133. chython/periodictable/__init__.py +69 -0
  134. chython/periodictable/base/__init__.py +25 -0
  135. chython/periodictable/base/dynamic.py +149 -0
  136. chython/periodictable/base/element.py +451 -0
  137. chython/periodictable/base/groups.py +90 -0
  138. chython/periodictable/base/periods.py +46 -0
  139. chython/periodictable/base/query.py +473 -0
  140. chython/periodictable/base/vector.py +119 -0
  141. chython/periodictable/groupI.py +252 -0
  142. chython/periodictable/groupII.py +223 -0
  143. chython/periodictable/groupIII.py +1154 -0
  144. chython/periodictable/groupIV.py +210 -0
  145. chython/periodictable/groupIX.py +196 -0
  146. chython/periodictable/groupV.py +192 -0
  147. chython/periodictable/groupVI.py +183 -0
  148. chython/periodictable/groupVII.py +162 -0
  149. chython/periodictable/groupVIII.py +161 -0
  150. chython/periodictable/groupX.py +166 -0
  151. chython/periodictable/groupXI.py +164 -0
  152. chython/periodictable/groupXII.py +158 -0
  153. chython/periodictable/groupXIII.py +239 -0
  154. chython/periodictable/groupXIV.py +262 -0
  155. chython/periodictable/groupXV.py +276 -0
  156. chython/periodictable/groupXVI.py +457 -0
  157. chython/periodictable/groupXVII.py +324 -0
  158. chython/periodictable/groupXVIII.py +260 -0
  159. chython/periodictable/test/__init__.py +0 -0
  160. chython/periodictable/test/test_element.py +100 -0
  161. chython/reactor/__init__.py +24 -0
  162. chython/reactor/base.py +222 -0
  163. chython/reactor/reactor.py +177 -0
  164. chython/reactor/test/__init__.py +18 -0
  165. chython/reactor/test/test_reactor.py +40 -0
  166. chython/reactor/test/test_transformer.py +78 -0
  167. chython/reactor/transformer.py +72 -0
  168. chython/utils/__init__.py +50 -0
  169. chython/utils/free_wilson.py +114 -0
  170. chython/utils/grid.py +130 -0
  171. chython/utils/retro.py +139 -0
  172. chython-2.19.dist-info/METADATA +105 -0
  173. chython-2.19.dist-info/RECORD +175 -0
  174. chython-2.19.dist-info/WHEEL +4 -0
  175. chython-2.19.dist-info/licenses/LICENSE +165 -0
@@ -0,0 +1,188 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2022-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ from collections import ChainMap
20
+ from itertools import count , chain, repeat
21
+ from .attention import Attention
22
+ from .reconstruct import Reconstruct
23
+ from ._groups import xonyl_groups, substituents_groups
24
+ from ._reactions import rules
25
+
26
+
27
+ class Mapping(Reconstruct, Attention):
28
+ __slots__ = ()
29
+
30
+ def reset_mapping(self) -> bool:
31
+ """
32
+ Reset atom-to-atom mapping by remapping atoms to unique numbers.
33
+ """
34
+ r = [n for m in chain(self.reactants, self.reagents) for n in m._atoms]
35
+ p = [n for m in self.products for n in m._atoms]
36
+ c = count(1)
37
+ if len(r) != len(set(r)):
38
+ for m in chain(self.reactants, self.reagents):
39
+ m.remap({n: next(c) for n in m._atoms})
40
+ if len(p) != len(set(p)):
41
+ for m in self.products:
42
+ m.remap({n: next(c) for n in m._atoms})
43
+ if next(c) != 1:
44
+ self.flush_cache()
45
+ return True
46
+ return False
47
+
48
+ def fix_mapping(self, *, logging: bool = False) -> bool | list:
49
+ """
50
+ Fix mapping of plipped functional groups and common mechanism errors.
51
+ """
52
+ if not self:
53
+ if logging:
54
+ return []
55
+ return False
56
+ log = self.__fix_groups()
57
+ log.extend(self.__fix_cgr())
58
+ if logging:
59
+ return log
60
+ return bool(log)
61
+
62
+ def __fix_cgr(self):
63
+ cgr = ~self
64
+ if not cgr.center_atoms:
65
+ return []
66
+
67
+ log = []
68
+ free_number = count(max(cgr) + 1)
69
+ components = [(cgr.substructure(c),
70
+ cgr.augmented_substructure(c, 2), # deep DEPENDS on rules!
71
+ c)
72
+ for c in cgr.substructure(cgr.center_atoms).connected_components]
73
+
74
+ r_atoms = ChainMap(*(x._atoms for x in self.reactants))
75
+ for c, ac, cs in components:
76
+ for rule_num, (query, signature, restrict, fix) in enumerate(rules):
77
+ if str(c) == signature:
78
+ for mapping in query.get_mapping(ac, automorphism_filter=False):
79
+ if not cs.issubset(mapping.values()):
80
+ continue
81
+ if restrict is not None and any(a != r_atoms.get(mapping[n]) for n, a in restrict.atoms()):
82
+ continue
83
+ mapping = {mapping[n]: next(free_number) if m is None else mapping[m] for n, m in fix.items()}
84
+ for m in self.products:
85
+ m.remap(mapping)
86
+ log.append((rule_num, signature, tuple(mapping.values())))
87
+ break
88
+ else:
89
+ continue
90
+ break # component remapped!
91
+
92
+ if log:
93
+ self.flush_cache()
94
+ return log
95
+
96
+ def __fix_groups(self):
97
+ log = []
98
+ seen = set()
99
+ remap = {}
100
+ pamer = {}
101
+ r_groups = set()
102
+ p_groups = set()
103
+ r_subs = set()
104
+ p_subs = set()
105
+
106
+ # find xonyl groups. any charged-neutral combinations
107
+ for pattern in xonyl_groups:
108
+ for m, g in chain(zip(self.reactants, repeat(r_groups)), zip(self.products, repeat(p_groups))):
109
+ atoms = m._atoms
110
+ for mapping in pattern.get_mapping(m, automorphism_filter=False):
111
+ n1, n2, n3 = mapping[1], mapping[2], mapping[3]
112
+ if (t := atoms[n2].atomic_number) == atoms[n3].atomic_number:
113
+ g.add((n1, n2, n3, atoms[n1].atomic_number, t))
114
+
115
+ for pattern, _map in substituents_groups:
116
+ for m, g in chain(zip(self.reactants, repeat(r_subs)), zip(self.products, repeat(p_subs))):
117
+ atoms = m._atoms
118
+ for mapping in pattern.get_mapping(m, automorphism_filter=False):
119
+ g.add((n := mapping[1], atoms[n].atomic_number,
120
+ tuple((n := mapping[x], y - 2, atoms[n].atomic_number) for x, y in _map), m))
121
+
122
+ r_groups = list(r_groups)
123
+ p_groups = list(p_groups)
124
+
125
+ # find pairs
126
+ if r_groups and p_groups:
127
+ for n1, n2, n3, x1, x2 in r_groups:
128
+ if n1 in seen: # already remapped
129
+ continue
130
+ for i, (m1, m2, m3, y1, y2) in enumerate(p_groups):
131
+ if m1 not in seen and n1 == m1 and x1 == y1 and x2 == y2: # found pair
132
+ if n2 == m3 and n3 == m2: # found switch
133
+ remap[m2] = m3
134
+ remap[m3] = m2
135
+ seen.add(n1)
136
+ break
137
+ else:
138
+ continue
139
+ del p_groups[i]
140
+
141
+ if not p_groups: # optimize
142
+ r_subs.clear()
143
+
144
+ # hydrolysis, etc.
145
+ for (n1, x1, _map, m), g, r in chain(zip(r_subs, repeat(p_groups), repeat(remap)),
146
+ zip(p_subs, repeat(r_groups), repeat(pamer))):
147
+ if n1 in seen:
148
+ continue
149
+ for i, (m1, *m23, y1, y2) in enumerate(g):
150
+ if m1 not in seen and n1 == m1 and x1 == y1: # found center
151
+ if len(_map) == 1: # acids substitutions.
152
+ ni, _, xi = _map[0]
153
+ # second neighbor should be disconnected from central atom.
154
+ if xi == y2 and m23[0] == ni and (m23[1] not in m._atoms or m23[1] not in m._bonds[n1]):
155
+ m2, m3 = m23
156
+ r[m2] = m3
157
+ r[m3] = m2
158
+ seen.add(n1)
159
+ break
160
+ elif all(xi == y2 and m23[mi] == ni for ni, mi, xi in _map):
161
+ m2, m3 = m23
162
+ r[m2] = m3
163
+ r[m3] = m2
164
+ seen.add(n1)
165
+ break
166
+ else:
167
+ continue
168
+ del g[i]
169
+
170
+ if remap:
171
+ seen = set(remap)
172
+ for m in self.products:
173
+ if not seen.isdisjoint(m):
174
+ m.remap(remap)
175
+ log.append(('products groups remapped', tuple(remap)))
176
+ if pamer:
177
+ seen = set(pamer)
178
+ for m in self.reactants:
179
+ if not seen.isdisjoint(m):
180
+ m.remap(pamer)
181
+ log.append(('reactants groups remapped', tuple(pamer)))
182
+
183
+ if log:
184
+ self.flush_cache()
185
+ return log
186
+
187
+
188
+ __all__ = ['Mapping']
@@ -0,0 +1,94 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2021-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ from lazy_object_proxy import Proxy
20
+
21
+
22
+ def _xonyl_groups():
23
+ from ... import smarts
24
+
25
+ rules = []
26
+ # atom 1 - axis
27
+ # atoms 2, 3 - swapping
28
+ q = smarts('[N;D3;z2;+]([O;D1;-])=[O;D1]')
29
+ rules.append(q)
30
+
31
+ q = smarts('[N,O,S,Se;D1;z2:3]=[C,P,As,Sb,Bi,S,Se,Te,Po,Cl,Br,I,At:1][N,O,S,Se;D1:2]')
32
+ rules.append(q)
33
+
34
+ q = smarts('[N,O,S,Se;D1;z2:3]=[C,P,As,Sb,Bi,S,Se,Te,Po,Cl,Br,I,At:1][N,O,S,Se;D1;-:2]')
35
+ rules.append(q)
36
+ return rules
37
+
38
+
39
+ def _substituents():
40
+ """
41
+ Rules for switchable functional groups remapping
42
+ """
43
+ from ... import smarts
44
+
45
+ rules = []
46
+
47
+ # nitro addition
48
+ #
49
+ # O [3] [2] O -- *
50
+ # // /:
51
+ # * - N+ >> * = N+
52
+ # \ \
53
+ # O- [2] O- [3]
54
+ #
55
+ q = smarts('[N;D3;z2,z4;+]([O;D1;-])-,:[O;D2]')
56
+ rules.append((q, ((2, 3), (3, 2))))
57
+
58
+ #
59
+ # NH [3] NH2 [3]
60
+ # // /
61
+ # * - X >> * - X
62
+ # \ :
63
+ # NH2 [2] N [2]
64
+ #
65
+ q = smarts('[N;D2;h0;z2,z4:3]=,:[C;D3:1][N:2]')
66
+ rules.append((q, ((2, 3), (3, 2))))
67
+
68
+ #
69
+ # O [3] [3] O - R
70
+ # // /
71
+ # * - X >> * - X
72
+ # \ \\
73
+ # OH [2] O [2]
74
+ #
75
+ q = smarts('[N,O,S,Se;D1;z2:3]=[C,P,As,Sb,Bi,S,Se,Te,Po,Cl,Br,I,At:1][N,O,S,Se;D2:2]')
76
+ rules.append((q, ((2, 3), (3, 2))))
77
+
78
+ #
79
+ # O [3] A
80
+ # // /
81
+ # * - X >> * - X
82
+ # \ \\
83
+ # OH [2] O [2]
84
+ #
85
+ q = smarts('[N,O,S,Se;D1;z2:3]=[C,P,As,Sb,Bi,S,Se,Te,Po,Cl,Br,I,At:1][A:2]')
86
+ rules.append((q, ((3, 2),))) # possible only: (3, 2)
87
+ return rules
88
+
89
+
90
+ xonyl_groups = Proxy(_xonyl_groups)
91
+ substituents_groups = Proxy(_substituents)
92
+
93
+
94
+ __all__ = ['xonyl_groups', 'substituents_groups']
@@ -0,0 +1,72 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2022-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ from lazy_object_proxy import Proxy
20
+
21
+
22
+ def _rules():
23
+ from ... import smiles, smarts
24
+ raw_rules = []
25
+
26
+ # phenol etherification
27
+ r = smiles('[OH:5][cH:1]:[cH2:2].[CH3:4][OH:3]>>[CH3:4][O:3][cH:1]:[cH2:2]')
28
+ m = smarts('[O;D1:5].[O;D1:3]') # reactant restriction.
29
+ raw_rules.append((r, {3: 5}, m))
30
+
31
+ # esterification
32
+ r = smiles('[OH:2][CH:1]=[O:3].[CH3:4][OH:5]>>[CH3:4][O:2][CH:1]=[O:3]')
33
+ m = smarts('[O;D1:2].[O;D1:5]')
34
+ raw_rules.append((r, {2: 5}, m))
35
+
36
+ # acid reduction
37
+ r = smiles('[CH3:2][C:1]([OH:3])=[O:4]>>[CH3:2][CH2:1][OH:3]')
38
+ m = smarts('[O;D1:3]')
39
+ raw_rules.append((r, {3: 4, 4: 3}, m))
40
+
41
+ # ester reduction
42
+ r = smiles('[CH3:5][O:3][C:1]([CH3:2])=[O:4]>>[CH3:2][CH2:1][OH:3]')
43
+ raw_rules.append((r, {3: 4}, None))
44
+
45
+ # ozonolysis
46
+ r = smiles('[O-:4][O+:1]=[O:6].[CH3:3][CH:2]=[CH2:5]>>[CH3:3][CH:2]=[O:1]')
47
+ raw_rules.append((r, {1: 4}, None))
48
+
49
+ # peroxide oxirane
50
+ r = smiles('[CH3:6][O:1][OH:5].[CH3:4][CH:2]=[CH2:3]>>[CH3:4][CH:2]1[CH2:3][O:1]1')
51
+ m = smarts('[O;D1:5]')
52
+ raw_rules.append((r, {1: 5}, m))
53
+
54
+ # claisen rearrangement
55
+ r = smiles('[cH2:4]:[cH:3]:[cH:2][O:1][CH2:5][CH:6]=[CH2:7]>>[OH:1][cH:2]:[c:3](:[cH2:4])[CH2:5][CH:6]=[CH2:7]')
56
+ raw_rules.append((r, {5: 7, 7: 5}, None))
57
+
58
+ # metathesis
59
+ r = smiles('[CH3:1][CH:2]=[CH2:3].[CH3:5][CH:4]=[CH2:6]>>[CH3:5][CH:3]=[CH:2][CH3:1]')
60
+ raw_rules.append((r, {3: 4, 4: 3}, None))
61
+
62
+ rules = []
63
+ for r, f, m in raw_rules:
64
+ c = ~r
65
+ rules.append((c, str(c.substructure(c.center_atoms)), m, f))
66
+ return rules
67
+
68
+
69
+ rules = Proxy(_rules)
70
+
71
+
72
+ __all__ = ['rules']
@@ -0,0 +1,266 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2022-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # Copyright 2024 Philippe Gantzer <p.gantzer@icredd.hokudai.ac.jp>
5
+ # This file is part of chython.
6
+ #
7
+ # chython is free software; you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation; either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
19
+ #
20
+ from functools import cache
21
+ from itertools import repeat
22
+ from logging import getLogger, INFO
23
+ from numpy import array, argmax, clip, concatenate, int32, int64, isclose, ix_, mean, nan_to_num
24
+ from numpy import nonzero, ones, unravel_index, zeros
25
+ from scipy.sparse.csgraph import shortest_path
26
+
27
+ logger = getLogger('chython.attention')
28
+ logger.setLevel(INFO)
29
+
30
+ _max_distance = 10
31
+ _max_neighbors = 14
32
+
33
+
34
+ @cache
35
+ def _get_session():
36
+ import onnxruntime as ort
37
+ from os import cpu_count
38
+
39
+ try:
40
+ from chython_rxnmap import model_path
41
+ except ImportError:
42
+ raise ImportError('chython-rxnmap package is required for attention mapping. '
43
+ 'Install it with: pip install chython-rxnmap')
44
+
45
+ opts = ort.SessionOptions()
46
+ opts.inter_op_num_threads = 1
47
+ opts.intra_op_num_threads = min(cpu_count() or 4, 8)
48
+ opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
49
+ return ort.InferenceSession(model_path, opts, providers=['CPUExecutionProvider'])
50
+
51
+
52
+ def _run_model(reactants, products, p2r, r2p, equal_atoms):
53
+ """Run ONNX model on reaction, return symmetrized attention filtered by atom type."""
54
+ atoms, neighbors, distances, roles = _encode_reaction(reactants, products)
55
+ am = _get_session().run(None, {
56
+ 'atoms': atoms[None].astype(int64),
57
+ 'neighbors': neighbors[None].astype(int64),
58
+ 'distances': distances[None].astype(int64),
59
+ 'roles': roles[None].astype(int64),
60
+ })[0]
61
+ return (am[p2r] + am[r2p].T) * equal_atoms
62
+
63
+
64
+ def _encode_reaction(reactants, products):
65
+ """Encode reaction into model input tensors (atoms, neighbors, distances, roles)."""
66
+ atoms_list = [array([0], dtype=int32)] # rxn_cls
67
+ neighbors_list = [array([0], dtype=int32)]
68
+ roles_list = [1] # rxn_cls role
69
+ distances_list = []
70
+
71
+ for mol in reactants:
72
+ a, n, d = _encode_molecule(mol)
73
+ atoms_list.append(a)
74
+ neighbors_list.append(n)
75
+ distances_list.append(d)
76
+ roles_list.append(0) # mol_cls (hidden)
77
+ roles_list.extend(repeat(2, len(mol)))
78
+
79
+ for mol in products:
80
+ a, n, d = _encode_molecule(mol)
81
+ atoms_list.append(a)
82
+ neighbors_list.append(n)
83
+ distances_list.append(d)
84
+ roles_list.append(0)
85
+ roles_list.extend(repeat(3, len(mol)))
86
+
87
+ atoms = concatenate(atoms_list)
88
+ neighbors = concatenate(neighbors_list)
89
+ roles = array(roles_list, dtype=int32)
90
+
91
+ total = len(roles)
92
+ distances = zeros((total, total), dtype=int32)
93
+ distances[0, 0] = 1 # rxn_cls self-loop
94
+ i = 1
95
+ for d in distances_list:
96
+ j = i + d.shape[0]
97
+ distances[i:j, i:j] = d
98
+ i = j
99
+
100
+ return atoms, neighbors, distances, roles
101
+
102
+
103
+ def _encode_molecule(mol):
104
+ """Encode molecule into (atoms, neighbors, distances) tensors.
105
+
106
+ Encoding:
107
+ atoms: 0=padding, 1=mol_cls, 2+=atomic_number+2
108
+ neighbors: 0=cls, 2+=neighbor_count+2
109
+ distances: 0=padding, 1=unreachable/cross-component, 2+=shortest_path+2
110
+ """
111
+ n_atoms = len(mol)
112
+ size = n_atoms + 1 # +1 for mol_cls
113
+
114
+ atoms = zeros(size, dtype=int32)
115
+ neighbors = zeros(size, dtype=int32)
116
+ atoms[0] = 1 # mol_cls
117
+
118
+ bonds = mol._bonds
119
+ for i, (n, a) in enumerate(mol.atoms(), 1):
120
+ atoms[i] = a.atomic_number + 2
121
+ nb = len(bonds[n]) + (a.implicit_hydrogens or 0)
122
+ if nb > _max_neighbors:
123
+ nb = _max_neighbors
124
+ neighbors[i] = nb + 2
125
+
126
+ adj = mol.adjacency_matrix()
127
+ dist = shortest_path(adj, method='FW', directed=False, unweighted=True)
128
+ nan_to_num(dist, copy=False, posinf=1.0) # unreachable -> 1 (cross-component attention)
129
+ clip(dist, None, _max_distance, out=dist)
130
+ dist = (dist + 2).astype(int32)
131
+
132
+ distances = ones((size, size), dtype=int32) # mol_cls distance=1 to all atoms
133
+ distances[1:, 1:] = dist
134
+ return atoms, neighbors, distances
135
+
136
+
137
+ def _prepare_masks(reactants, products, reagents):
138
+ """Build atom maps, adjacency matrices, element-equality mask, and token-level index slices."""
139
+ r_map = [n for m in reactants for n in m]
140
+ p_map = [n for m in products for n in m]
141
+ rg_map = [n for m in reagents for n in m]
142
+ ra = len(r_map)
143
+ pa = len(p_map)
144
+
145
+ # token layout: [rxn_cls, mol_cls_1, atoms_1, ..., mol_cls_n, atoms_n, ...]
146
+ ram = [False] # rxn_cls
147
+ r_atoms = []
148
+ r_adj = zeros((ra, ra), dtype=bool)
149
+ i = 0
150
+ for m in reactants:
151
+ ram.append(False) # mol_cls
152
+ ram.extend(repeat(True, len(m)))
153
+ j = i + len(m)
154
+ r_adj[i:j, i:j] = m.adjacency_matrix()
155
+ i = j
156
+ r_atoms.extend(a.atomic_number for _, a in m.atoms())
157
+
158
+ pam = [False] * len(ram)
159
+ p_atoms = []
160
+ p_adj = zeros((pa, pa), dtype=bool)
161
+ i = 0
162
+ for m in products:
163
+ pam.append(False)
164
+ pam.extend(repeat(True, len(m)))
165
+ j = i + len(m)
166
+ p_adj[i:j, i:j] = m.adjacency_matrix()
167
+ i = j
168
+ p_atoms.extend(a.atomic_number for _, a in m.atoms())
169
+
170
+ ram.extend(repeat(False, len(pam) - len(ram)))
171
+ ram = array(ram, dtype=bool)
172
+ pam = array(pam, dtype=bool)
173
+ r_atoms = array(r_atoms, dtype=int)
174
+ p_atoms = array(p_atoms, dtype=int)
175
+
176
+ equal_atoms = p_atoms[:, None] == r_atoms
177
+ return r_map, p_map, rg_map, equal_atoms, ix_(pam, ram), ix_(ram, pam), r_adj, p_adj
178
+
179
+
180
+ def _greedy_mapping(am, r_map, p_map, r_adj, p_adj, multiplier):
181
+ """Greedy attention-based product-to-reactant atom assignment."""
182
+ amc = am.copy()
183
+ pa = len(p_map)
184
+ mapping = {}
185
+ scope = zeros(pa, dtype=bool)
186
+ seen = ones(pa, dtype=bool)
187
+ score = []
188
+
189
+ for x in range(pa):
190
+ if not x:
191
+ i, j = unravel_index(argmax(am), am.shape)
192
+ else:
193
+ ams = am[scope]
194
+ if ams.size:
195
+ i, j = unravel_index(argmax(ams), ams.shape)
196
+ i = nonzero(scope)[0][i]
197
+ else:
198
+ i, j = unravel_index(argmax(am), am.shape)
199
+
200
+ if isclose(am[i, j], 0.):
201
+ for n in set(p_map).difference(mapping):
202
+ mapping[n] = 0
203
+ break
204
+
205
+ score.append(amc[i, j])
206
+ mapping[p_map[i]] = r_map[j]
207
+ am[ix_(p_adj[i], r_adj[j])] *= multiplier
208
+ am[i] = am[:, j] = 0
209
+ seen[i] = False
210
+ scope[i] = False
211
+ scope[p_adj[i] & seen] = True
212
+
213
+ return mapping, float(mean(score)) if score else 0.
214
+
215
+
216
+ class Attention:
217
+ __slots__ = ()
218
+
219
+ def attention_mapping(self, *, return_score: bool = False, multiplier=1.75,
220
+ keep_reactants_numbering=False) -> bool | float:
221
+ """Do atom-to-atom mapping. Return True if mapping changed."""
222
+ if any(len(bs) > _max_neighbors for m in self.molecules() for bs in m._bonds.values()):
223
+ logger.info('atom-to-atom mapping not supported for hypervalent compounds')
224
+ return False
225
+
226
+ fixed = self.reset_mapping()
227
+ r_map, p_map, rg_map, equal_atoms, p2r, r2p, r_adj, p_adj = _prepare_masks(
228
+ self.reactants, self.products, self.reagents
229
+ )
230
+
231
+ am = _run_model(self.reactants, self.products, p2r, r2p, equal_atoms)
232
+ mapping, score = _greedy_mapping(am, r_map, p_map, r_adj, p_adj, multiplier)
233
+
234
+ if any(n != m for n, m in mapping.items()):
235
+ if keep_reactants_numbering or fixed:
236
+ r_mapping = {n: n for n in r_map}
237
+ else:
238
+ r_mapping = {m: n for n, m in enumerate(r_map, 1)}
239
+ for m in self.reactants:
240
+ m.remap(r_mapping)
241
+
242
+ p_mapping = {}
243
+ nm = max(r_mapping.values())
244
+ for n, m in mapping.items():
245
+ if m := r_mapping.get(m):
246
+ p_mapping[n] = m
247
+ else:
248
+ nm += 1
249
+ p_mapping[n] = nm
250
+ for m in self.products:
251
+ m.remap(p_mapping)
252
+
253
+ if not keep_reactants_numbering and not fixed:
254
+ rg_mapping = {m: n for n, m in enumerate(rg_map, nm + 1)}
255
+ for m in self.reagents:
256
+ m.remap(rg_mapping)
257
+
258
+ self.flush_cache()
259
+ fixed = True
260
+
261
+ if self.fix_mapping():
262
+ fixed = True
263
+ return score if return_score else fixed
264
+
265
+
266
+ __all__ = ['Attention']