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.
- chython/__init__.py +35 -0
- chython/_functions.py +95 -0
- chython/_java.py +57 -0
- chython/algorithms/__init__.py +18 -0
- chython/algorithms/_isomorphism.cp313-win_amd64.pyd +0 -0
- chython/algorithms/_isomorphism.pyx +189 -0
- chython/algorithms/_rings.cp313-win_amd64.pyd +0 -0
- chython/algorithms/_rings.pyx +478 -0
- chython/algorithms/aromatics/__init__.py +27 -0
- chython/algorithms/aromatics/_rules.py +124 -0
- chython/algorithms/aromatics/kekule.py +532 -0
- chython/algorithms/aromatics/test/__init__.py +18 -0
- chython/algorithms/aromatics/test/test_kekule.py +160 -0
- chython/algorithms/aromatics/test/test_thiele.py +263 -0
- chython/algorithms/aromatics/thiele.py +222 -0
- chython/algorithms/calculate2d/__init__.py +23 -0
- chython/algorithms/calculate2d/clean2d.js +1 -0
- chython/algorithms/calculate2d/molecule.py +191 -0
- chython/algorithms/calculate2d/reaction.py +75 -0
- chython/algorithms/conformers.py +106 -0
- chython/algorithms/depict.py +596 -0
- chython/algorithms/fingerprints/__init__.py +43 -0
- chython/algorithms/fingerprints/linear.py +202 -0
- chython/algorithms/fingerprints/morgan.py +132 -0
- chython/algorithms/fingerprints/test/__init__.py +18 -0
- chython/algorithms/fingerprints/test/test_linear.py +127 -0
- chython/algorithms/fingerprints/test/test_morgan.py +79 -0
- chython/algorithms/groups/__init__.py +212 -0
- chython/algorithms/groups/_functional.py +290 -0
- chython/algorithms/groups/_oxidations.py +70 -0
- chython/algorithms/groups/_protective.py +964 -0
- chython/algorithms/groups/_reactions.py +809 -0
- chython/algorithms/groups/_reductions.py +88 -0
- chython/algorithms/groups/_transformations.py +278 -0
- chython/algorithms/groups/test/__init__.py +18 -0
- chython/algorithms/groups/test/test_deprotection.py +108 -0
- chython/algorithms/groups/test/test_reactions.py +377 -0
- chython/algorithms/isomorphism.py +664 -0
- chython/algorithms/mapping/__init__.py +188 -0
- chython/algorithms/mapping/_groups.py +94 -0
- chython/algorithms/mapping/_reactions.py +72 -0
- chython/algorithms/mapping/attention.py +266 -0
- chython/algorithms/mapping/reconstruct.py +346 -0
- chython/algorithms/mcs.py +201 -0
- chython/algorithms/morgan.py +77 -0
- chython/algorithms/rings.py +178 -0
- chython/algorithms/smiles.py +597 -0
- chython/algorithms/standardize/__init__.py +30 -0
- chython/algorithms/standardize/_groups.py +958 -0
- chython/algorithms/standardize/_isomers.py +330 -0
- chython/algorithms/standardize/_metal_organics.py +189 -0
- chython/algorithms/standardize/_reagents.py +44 -0
- chython/algorithms/standardize/_salts.py +68 -0
- chython/algorithms/standardize/molecule.py +475 -0
- chython/algorithms/standardize/reaction.py +384 -0
- chython/algorithms/standardize/resonance.py +191 -0
- chython/algorithms/standardize/salts.py +128 -0
- chython/algorithms/standardize/saturation.py +373 -0
- chython/algorithms/standardize/test/__init__.py +18 -0
- chython/algorithms/standardize/test/test_canonicalize.py +66 -0
- chython/algorithms/standardize/test/test_groups.py +146 -0
- chython/algorithms/standardize/test/test_isomers.py +198 -0
- chython/algorithms/stereo.py +1215 -0
- chython/algorithms/tautomers/__init__.py +167 -0
- chython/algorithms/tautomers/_acid.py +59 -0
- chython/algorithms/tautomers/_base.py +116 -0
- chython/algorithms/tautomers/_keto_enol.py +107 -0
- chython/algorithms/tautomers/acid_base.py +208 -0
- chython/algorithms/tautomers/heteroarenes.py +103 -0
- chython/algorithms/tautomers/keto_enol.py +143 -0
- chython/algorithms/tautomers/test/__init__.py +18 -0
- chython/algorithms/tautomers/test/test_tautomers.py +80 -0
- chython/algorithms/test/__init__.py +18 -0
- chython/algorithms/test/test_isomorphism.py +58 -0
- chython/algorithms/test/test_smiles.py +76 -0
- chython/algorithms/x3dom.py +350 -0
- chython/containers/__init__.py +48 -0
- chython/containers/_pack_v2.cp313-win_amd64.pyd +0 -0
- chython/containers/_pack_v2.pyx +271 -0
- chython/containers/_unpack_v0v2.cp313-win_amd64.pyd +0 -0
- chython/containers/_unpack_v0v2.pyx +302 -0
- chython/containers/bonds.py +255 -0
- chython/containers/cgr.py +115 -0
- chython/containers/chimera.py +63 -0
- chython/containers/graph.py +199 -0
- chython/containers/molecule.py +817 -0
- chython/containers/query.py +71 -0
- chython/containers/rdkit.py +183 -0
- chython/containers/reaction.py +324 -0
- chython/containers/test/__init__.py +18 -0
- chython/containers/test/test_molecule.py +117 -0
- chython/containers/test/test_pack.py +60 -0
- chython/containers/test/test_rdkit.py +70 -0
- chython/containers/test/test_reaction.py +40 -0
- chython/exceptions.py +114 -0
- chython/files/MRVrw.py +532 -0
- chython/files/PDBrw.py +255 -0
- chython/files/RDFrw.py +309 -0
- chython/files/SDFrw.py +221 -0
- chython/files/__init__.py +30 -0
- chython/files/_convert.py +207 -0
- chython/files/_mapping.py +117 -0
- chython/files/_xyz.cp313-win_amd64.pyd +0 -0
- chython/files/_xyz.pyx +74 -0
- chython/files/daylight/__init__.py +24 -0
- chython/files/daylight/parser.py +157 -0
- chython/files/daylight/smarts.py +107 -0
- chython/files/daylight/smiles.py +260 -0
- chython/files/daylight/test/__init__.py +18 -0
- chython/files/daylight/test/test_daylight_smarts.py +289 -0
- chython/files/daylight/test/test_daylight_smiles.py +101 -0
- chython/files/daylight/test/test_parser.py +118 -0
- chython/files/daylight/test/test_tokenize.py +77 -0
- chython/files/daylight/tokenize.py +376 -0
- chython/files/libinchi/__init__.py +22 -0
- chython/files/libinchi/libinchi.dll +0 -0
- chython/files/libinchi/wrapper.py +554 -0
- chython/files/mdl/__init__.py +25 -0
- chython/files/mdl/emol.py +251 -0
- chython/files/mdl/erxn.py +67 -0
- chython/files/mdl/mol.py +186 -0
- chython/files/mdl/read.py +223 -0
- chython/files/mdl/rxn.py +67 -0
- chython/files/mdl/stereo.py +63 -0
- chython/files/mdl/write.py +201 -0
- chython/files/opsin.py +35 -0
- chython/files/test/__init__.py +0 -0
- chython/files/test/test_inchi.py +27 -0
- chython/files/test/test_mrv.py +47 -0
- chython/files/test/test_sdf_rdf.py +64 -0
- chython/files/test/test_xyz.py +20 -0
- chython/files/xyz.py +78 -0
- chython/periodictable/__init__.py +69 -0
- chython/periodictable/base/__init__.py +25 -0
- chython/periodictable/base/dynamic.py +149 -0
- chython/periodictable/base/element.py +451 -0
- chython/periodictable/base/groups.py +90 -0
- chython/periodictable/base/periods.py +46 -0
- chython/periodictable/base/query.py +473 -0
- chython/periodictable/base/vector.py +119 -0
- chython/periodictable/groupI.py +252 -0
- chython/periodictable/groupII.py +223 -0
- chython/periodictable/groupIII.py +1154 -0
- chython/periodictable/groupIV.py +210 -0
- chython/periodictable/groupIX.py +196 -0
- chython/periodictable/groupV.py +192 -0
- chython/periodictable/groupVI.py +183 -0
- chython/periodictable/groupVII.py +162 -0
- chython/periodictable/groupVIII.py +161 -0
- chython/periodictable/groupX.py +166 -0
- chython/periodictable/groupXI.py +164 -0
- chython/periodictable/groupXII.py +158 -0
- chython/periodictable/groupXIII.py +239 -0
- chython/periodictable/groupXIV.py +262 -0
- chython/periodictable/groupXV.py +276 -0
- chython/periodictable/groupXVI.py +457 -0
- chython/periodictable/groupXVII.py +324 -0
- chython/periodictable/groupXVIII.py +260 -0
- chython/periodictable/test/__init__.py +0 -0
- chython/periodictable/test/test_element.py +100 -0
- chython/reactor/__init__.py +24 -0
- chython/reactor/base.py +222 -0
- chython/reactor/reactor.py +177 -0
- chython/reactor/test/__init__.py +18 -0
- chython/reactor/test/test_reactor.py +40 -0
- chython/reactor/test/test_transformer.py +78 -0
- chython/reactor/transformer.py +72 -0
- chython/utils/__init__.py +50 -0
- chython/utils/free_wilson.py +114 -0
- chython/utils/grid.py +130 -0
- chython/utils/retro.py +139 -0
- chython-2.19.dist-info/METADATA +105 -0
- chython-2.19.dist-info/RECORD +175 -0
- chython-2.19.dist-info/WHEEL +4 -0
- 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']
|