chython 2.4__cp312-cp312-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 +31 -0
- chython/_functions.py +69 -0
- chython/algorithms/__init__.py +18 -0
- chython/algorithms/_isomorphism.cp312-win_amd64.pyd +0 -0
- chython/algorithms/_isomorphism.pyx +189 -0
- chython/algorithms/aromatics/__init__.py +27 -0
- chython/algorithms/aromatics/_rules.py +124 -0
- chython/algorithms/aromatics/kekule.py +530 -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 +226 -0
- chython/algorithms/calculate2d/__init__.py +23 -0
- chython/algorithms/calculate2d/clean2d.js +1 -0
- chython/algorithms/calculate2d/molecule.py +149 -0
- chython/algorithms/calculate2d/reaction.py +80 -0
- chython/algorithms/depict.py +523 -0
- chython/algorithms/fingerprints/__init__.py +48 -0
- chython/algorithms/fingerprints/linear.py +208 -0
- chython/algorithms/fingerprints/morgan.py +137 -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/isomorphism.py +667 -0
- chython/algorithms/mapping/__init__.py +28 -0
- chython/algorithms/mapping/_groups.py +94 -0
- chython/algorithms/mapping/_reactions.py +72 -0
- chython/algorithms/mapping/attention.py +216 -0
- chython/algorithms/mapping/fixmapper.py +84 -0
- chython/algorithms/mapping/groups.py +137 -0
- chython/algorithms/mcs.py +202 -0
- chython/algorithms/morgan.py +82 -0
- chython/algorithms/rings.py +551 -0
- chython/algorithms/smiles.py +556 -0
- chython/algorithms/standardize/__init__.py +30 -0
- chython/algorithms/standardize/_charged.py +115 -0
- chython/algorithms/standardize/_groups.py +926 -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 +479 -0
- chython/algorithms/standardize/reaction.py +429 -0
- chython/algorithms/standardize/resonance.py +196 -0
- chython/algorithms/standardize/salts.py +128 -0
- chython/algorithms/standardize/saturation.py +378 -0
- chython/algorithms/standardize/test/__init__.py +18 -0
- chython/algorithms/standardize/test/test_groups.py +118 -0
- chython/algorithms/stereo.py +1222 -0
- chython/algorithms/tautomers/__init__.py +171 -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 +214 -0
- chython/algorithms/tautomers/heteroarenes.py +108 -0
- chython/algorithms/tautomers/keto_enol.py +148 -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 +355 -0
- chython/containers/__init__.py +43 -0
- chython/containers/_pack_v2.cp312-win_amd64.pyd +0 -0
- chython/containers/_pack_v2.pyx +271 -0
- chython/containers/_unpack_v0v2.cp312-win_amd64.pyd +0 -0
- chython/containers/_unpack_v0v2.pyx +300 -0
- chython/containers/bonds.py +255 -0
- chython/containers/cgr.py +115 -0
- chython/containers/graph.py +200 -0
- chython/containers/molecule.py +695 -0
- chython/containers/query.py +66 -0
- chython/containers/reaction.py +324 -0
- chython/exceptions.py +114 -0
- chython/files/MRVrw.py +523 -0
- chython/files/PDBrw.py +257 -0
- chython/files/RDFrw.py +298 -0
- chython/files/SDFrw.py +221 -0
- chython/files/__init__.py +29 -0
- chython/files/_convert.py +207 -0
- chython/files/_mapping.py +117 -0
- chython/files/_xyz.cp312-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 +245 -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 +555 -0
- chython/files/mdl/__init__.py +25 -0
- chython/files/mdl/emol.py +209 -0
- chython/files/mdl/erxn.py +67 -0
- chython/files/mdl/mol.py +144 -0
- chython/files/mdl/read.py +225 -0
- chython/files/mdl/rxn.py +67 -0
- chython/files/mdl/stereo.py +63 -0
- chython/files/mdl/write.py +165 -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 +452 -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/reactor/__init__.py +24 -0
- chython/reactor/base.py +212 -0
- chython/reactor/deprotection.py +565 -0
- chython/reactor/groups.py +128 -0
- chython/reactor/reactions/__init__.py +157 -0
- chython/reactor/reactions/_amidation.py +58 -0
- chython/reactor/reactions/_amine_isocyanate.py +52 -0
- chython/reactor/reactions/_buchwald_hartwig.py +50 -0
- chython/reactor/reactions/_esterification.py +49 -0
- chython/reactor/reactions/_macmillan.py +43 -0
- chython/reactor/reactions/_reductive_amination.py +50 -0
- chython/reactor/reactions/_sonogashira.py +49 -0
- chython/reactor/reactions/_sulfonamidation.py +52 -0
- chython/reactor/reactions/_suzuki_miyaura.py +88 -0
- chython/reactor/reactions/ufe.py +99 -0
- chython/reactor/reactor.py +170 -0
- chython/reactor/retro/__init__.py +77 -0
- chython/reactor/retro/_amidation.py +39 -0
- chython/reactor/retro/_aryl_amination.py +32 -0
- chython/reactor/retro/_mitsunobu.py +49 -0
- chython/reactor/retro/_sonogashira.py +49 -0
- chython/reactor/retro/_suzuki_miyaura.py +54 -0
- chython/reactor/scaffold.py +122 -0
- chython/reactor/test/__init__.py +18 -0
- chython/reactor/test/test_deprotection.py +57 -0
- chython/reactor/test/test_reactor.py +40 -0
- chython/reactor/test/test_scaffold.py +57 -0
- chython/reactor/test/test_transformer.py +40 -0
- chython/reactor/transformer.py +62 -0
- chython/utils/__init__.py +57 -0
- chython/utils/free_wilson.py +114 -0
- chython/utils/grid.py +130 -0
- chython/utils/rdkit.py +165 -0
- chython/utils/retro.py +139 -0
- chython/utils/svg.py +57 -0
- chython/utils/test/__init__.py +18 -0
- chython/utils/test/test_rdkit.py +71 -0
- chython-2.4.dist-info/LICENSE +165 -0
- chython-2.4.dist-info/METADATA +101 -0
- chython-2.4.dist-info/RECORD +171 -0
- chython-2.4.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2018-2024 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2021 Timur Gimadiev <timur.gimadiev@gmail.com>
|
|
5
|
+
# Copyright 2024 Philippe Gantzer <p.gantzer@icredd.hokudai.ac.jp>
|
|
6
|
+
# This file is part of chython.
|
|
7
|
+
#
|
|
8
|
+
# chython is free software; you can redistribute it and/or modify
|
|
9
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
10
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
# (at your option) any later version.
|
|
12
|
+
#
|
|
13
|
+
# This program is distributed in the hope that it will be useful,
|
|
14
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
# GNU Lesser General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
19
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
#
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from typing import List, Tuple, TYPE_CHECKING, Union
|
|
23
|
+
from ._reagents import *
|
|
24
|
+
from ...exceptions import MappingError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from chython import ReactionContainer
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StandardizeReaction:
|
|
32
|
+
__slots__ = ()
|
|
33
|
+
|
|
34
|
+
def canonicalize(self: 'ReactionContainer', *, fix_mapping: bool = True, logging=False, fix_tautomers=True) -> \
|
|
35
|
+
Union[bool, List[Tuple[int, Tuple[int, ...], int, str]]]:
|
|
36
|
+
"""
|
|
37
|
+
Convert molecules to canonical forms of functional groups and aromatic rings without explicit hydrogens.
|
|
38
|
+
Return True if in any molecule found not canonical group.
|
|
39
|
+
|
|
40
|
+
:param fix_mapping: Search AAM errors of functional groups.
|
|
41
|
+
:param logging: return log from molecules with index of molecule.
|
|
42
|
+
Otherwise, return True if these groups found in any molecule.
|
|
43
|
+
:param fix_tautomers: convert tautomers to canonical forms.
|
|
44
|
+
"""
|
|
45
|
+
total = []
|
|
46
|
+
for n, m in enumerate(self.molecules()):
|
|
47
|
+
total.extend((n, *x) for x in m.canonicalize(logging=True, fix_tautomers=fix_tautomers))
|
|
48
|
+
|
|
49
|
+
if fix_mapping:
|
|
50
|
+
total.extend((-1, x, -1, m) for m, x in self.fix_groups_mapping(logging=True))
|
|
51
|
+
|
|
52
|
+
if total:
|
|
53
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
54
|
+
if logging:
|
|
55
|
+
return total
|
|
56
|
+
return bool(total)
|
|
57
|
+
|
|
58
|
+
def standardize(self: 'ReactionContainer', *, fix_mapping: bool = True, logging=False, fix_tautomers=True) -> \
|
|
59
|
+
Union[bool, List[Tuple[int, Tuple[int, ...], int, str]]]:
|
|
60
|
+
"""
|
|
61
|
+
Fix functional groups representation.
|
|
62
|
+
Return True if in any molecule fixed group.
|
|
63
|
+
|
|
64
|
+
Deprecated method. Use `canonicalize` directly.
|
|
65
|
+
|
|
66
|
+
:param fix_mapping: Search AAM errors of functional groups.
|
|
67
|
+
:param logging: return log from molecules with index of molecule.
|
|
68
|
+
Otherwise, return True if these groups found in any molecule.
|
|
69
|
+
:param fix_tautomers: convert tautomers to canonical forms.
|
|
70
|
+
"""
|
|
71
|
+
total = []
|
|
72
|
+
for n, m in enumerate(self.molecules()):
|
|
73
|
+
total.extend((n, *x) for x in m.standardize(logging=True, fix_tautomers=fix_tautomers))
|
|
74
|
+
|
|
75
|
+
if fix_mapping:
|
|
76
|
+
total.extend((-1, x, -1, m) for m, x in self.fix_groups_mapping(logging=True))
|
|
77
|
+
|
|
78
|
+
if total:
|
|
79
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
80
|
+
if logging:
|
|
81
|
+
return total
|
|
82
|
+
return bool(total)
|
|
83
|
+
|
|
84
|
+
def thiele(self: 'ReactionContainer', *, fix_tautomers=True) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Convert structures to aromatic form.
|
|
87
|
+
Return True if in any molecule found kekule ring
|
|
88
|
+
|
|
89
|
+
:param fix_tautomers: convert tautomers to canonical forms.
|
|
90
|
+
"""
|
|
91
|
+
total = False
|
|
92
|
+
for m in self.molecules():
|
|
93
|
+
if m.thiele(fix_tautomers=fix_tautomers):
|
|
94
|
+
total = True
|
|
95
|
+
if total:
|
|
96
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
97
|
+
return total
|
|
98
|
+
|
|
99
|
+
def kekule(self: 'ReactionContainer', *, buffer_size=7) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Convert structures to kekule form.
|
|
102
|
+
Return True if in any molecule found aromatic ring
|
|
103
|
+
|
|
104
|
+
:param buffer_size: number of attempts of pyridine form searching.
|
|
105
|
+
"""
|
|
106
|
+
total = False
|
|
107
|
+
for m in self.molecules():
|
|
108
|
+
if m.kekule(buffer_size=buffer_size):
|
|
109
|
+
total = True
|
|
110
|
+
if total:
|
|
111
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
112
|
+
return total
|
|
113
|
+
|
|
114
|
+
def clean_isotopes(self: 'ReactionContainer') -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Clean isotope marks for all molecules in reaction.
|
|
117
|
+
Returns True if in any molecule found isotope.
|
|
118
|
+
"""
|
|
119
|
+
flag = False
|
|
120
|
+
for m in self.molecules():
|
|
121
|
+
if m.clean_isotopes():
|
|
122
|
+
flag = True
|
|
123
|
+
if flag:
|
|
124
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
125
|
+
return flag
|
|
126
|
+
|
|
127
|
+
def clean_stereo(self: 'ReactionContainer'):
|
|
128
|
+
"""
|
|
129
|
+
Remove stereo data
|
|
130
|
+
"""
|
|
131
|
+
for m in self.molecules():
|
|
132
|
+
m.clean_stereo()
|
|
133
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
134
|
+
|
|
135
|
+
def check_valence(self: 'ReactionContainer') -> List[Tuple[int, Tuple[int, ...]]]:
|
|
136
|
+
"""
|
|
137
|
+
Check valences of all atoms of all molecules.
|
|
138
|
+
|
|
139
|
+
Works only on molecules with aromatic rings in Kekule form.
|
|
140
|
+
:return: list of invalid molecules with invalid atoms lists
|
|
141
|
+
"""
|
|
142
|
+
out = []
|
|
143
|
+
for n, m in enumerate(self.molecules()):
|
|
144
|
+
if c := m.check_valence():
|
|
145
|
+
out.append((n, tuple(c)))
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
def implicify_hydrogens(self: 'ReactionContainer') -> int:
|
|
149
|
+
"""
|
|
150
|
+
Remove explicit hydrogens if possible.
|
|
151
|
+
|
|
152
|
+
:return: number of removed hydrogens.
|
|
153
|
+
"""
|
|
154
|
+
total = 0
|
|
155
|
+
for m in self.molecules():
|
|
156
|
+
total += m.implicify_hydrogens()
|
|
157
|
+
if total:
|
|
158
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
159
|
+
return total
|
|
160
|
+
|
|
161
|
+
def explicify_hydrogens(self: 'ReactionContainer') -> int:
|
|
162
|
+
"""
|
|
163
|
+
Add explicit hydrogens to atoms
|
|
164
|
+
|
|
165
|
+
:return: number of added atoms
|
|
166
|
+
"""
|
|
167
|
+
total = 0
|
|
168
|
+
start_map = 0
|
|
169
|
+
for m in self.molecules():
|
|
170
|
+
map_ = max(m, default=0)
|
|
171
|
+
if map_ > start_map:
|
|
172
|
+
start_map = map_
|
|
173
|
+
|
|
174
|
+
mapping = defaultdict(list)
|
|
175
|
+
for m in self.reactants:
|
|
176
|
+
maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
|
|
177
|
+
if maps:
|
|
178
|
+
for n, h in maps:
|
|
179
|
+
mapping[n].append(h)
|
|
180
|
+
start_map = maps[-1][1]
|
|
181
|
+
total += len(maps)
|
|
182
|
+
|
|
183
|
+
for m in self.reagents:
|
|
184
|
+
maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
|
|
185
|
+
if maps:
|
|
186
|
+
start_map = maps[-1][1]
|
|
187
|
+
total += len(maps)
|
|
188
|
+
|
|
189
|
+
for m in self.products:
|
|
190
|
+
maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
|
|
191
|
+
if maps:
|
|
192
|
+
total += len(maps)
|
|
193
|
+
remap = {}
|
|
194
|
+
free = []
|
|
195
|
+
for n, h in maps:
|
|
196
|
+
if n in mapping and mapping[n]:
|
|
197
|
+
remap[h] = mapping[n].pop()
|
|
198
|
+
free.append(h)
|
|
199
|
+
elif free:
|
|
200
|
+
remap[h] = start_map = free.pop(0)
|
|
201
|
+
else:
|
|
202
|
+
start_map = h
|
|
203
|
+
m.remap(remap)
|
|
204
|
+
|
|
205
|
+
if total:
|
|
206
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
207
|
+
return total
|
|
208
|
+
|
|
209
|
+
def remove_reagents(self, *, keep_reagents: bool = False, mapping: bool = True) -> bool:
|
|
210
|
+
"""
|
|
211
|
+
Place molecules, except reactants, to reagents list. Reagents - molecules which atoms not presented in products.
|
|
212
|
+
Mapping based approach remove molecules without reaction center.
|
|
213
|
+
Rule based approach remove equal molecules in reactants and products, and predefined reactants.
|
|
214
|
+
|
|
215
|
+
:param mapping: use atom-to-atom mapping to detect reagents, otherwise use predefined list of common reagents.
|
|
216
|
+
:param keep_reagents: delete reagents if False
|
|
217
|
+
|
|
218
|
+
Return True if any reagent found.
|
|
219
|
+
"""
|
|
220
|
+
if mapping:
|
|
221
|
+
return self.__remove_reagents_mapping(keep_reagents)
|
|
222
|
+
return self.__remove_reagents_rules(keep_reagents)
|
|
223
|
+
|
|
224
|
+
def __remove_reagents_rules(self: 'ReactionContainer', keep_reagents):
|
|
225
|
+
if not self.reactants or not self.products: # there is no reaction
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
reactants_st1 = []
|
|
229
|
+
products_st1 = []
|
|
230
|
+
reagents_st1 = set(self.reagents)
|
|
231
|
+
|
|
232
|
+
# find the same molecules in reactants and products
|
|
233
|
+
for m in self.reactants:
|
|
234
|
+
if m in self.products:
|
|
235
|
+
reagents_st1.add(m)
|
|
236
|
+
else:
|
|
237
|
+
reactants_st1.append(m)
|
|
238
|
+
for m in self.products:
|
|
239
|
+
if m in self.reactants:
|
|
240
|
+
reagents_st1.add(m)
|
|
241
|
+
else:
|
|
242
|
+
products_st1.append(m)
|
|
243
|
+
if not reactants_st1 or not products_st1:
|
|
244
|
+
return False # keep bad reaction as is
|
|
245
|
+
|
|
246
|
+
reactants_st2 = []
|
|
247
|
+
products_st2 = []
|
|
248
|
+
reagents_st2 = reagents_st1.copy()
|
|
249
|
+
|
|
250
|
+
# filter out predefined reagents
|
|
251
|
+
for m in reactants_st1:
|
|
252
|
+
if m in reagents_set:
|
|
253
|
+
reagents_st2.add(m)
|
|
254
|
+
else:
|
|
255
|
+
reactants_st2.append(m)
|
|
256
|
+
for m in products_st1:
|
|
257
|
+
if m in reagents_set:
|
|
258
|
+
reagents_st2.add(m)
|
|
259
|
+
else:
|
|
260
|
+
products_st2.append(m)
|
|
261
|
+
if not reactants_st2 or not products_st2: # reaction contains only simple molecules. roll-back to step 1
|
|
262
|
+
reactants_st2 = reactants_st1
|
|
263
|
+
products_st2 = products_st1
|
|
264
|
+
reagents_st2 = reagents_st1
|
|
265
|
+
|
|
266
|
+
# remove reagents from reactants
|
|
267
|
+
tmp = []
|
|
268
|
+
for m in self.reagents:
|
|
269
|
+
tmp.append(m)
|
|
270
|
+
if m in reagents_st2:
|
|
271
|
+
reagents_st2.discard(m)
|
|
272
|
+
tmp.extend(reagents_st2)
|
|
273
|
+
reagents = tuple(tmp) if keep_reagents else ()
|
|
274
|
+
|
|
275
|
+
self._reactants = tuple(reactants_st2)
|
|
276
|
+
self._products = tuple(products_st2)
|
|
277
|
+
self._reagents = reagents
|
|
278
|
+
self.fix_positions()
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
def __remove_reagents_mapping(self: 'ReactionContainer', keep_reagents):
|
|
282
|
+
cgr = ~self
|
|
283
|
+
if cgr.center_atoms:
|
|
284
|
+
active = set(cgr.center_atoms)
|
|
285
|
+
reactants = []
|
|
286
|
+
products = []
|
|
287
|
+
reagents = set(self.reagents)
|
|
288
|
+
for i in self.reactants:
|
|
289
|
+
if not active.isdisjoint(i):
|
|
290
|
+
reactants.append(i)
|
|
291
|
+
else:
|
|
292
|
+
reagents.add(i)
|
|
293
|
+
for i in self.products:
|
|
294
|
+
if not active.isdisjoint(i):
|
|
295
|
+
products.append(i)
|
|
296
|
+
else:
|
|
297
|
+
reagents.add(i)
|
|
298
|
+
|
|
299
|
+
# remove reagents from reactants
|
|
300
|
+
tmp = []
|
|
301
|
+
for m in self.reagents:
|
|
302
|
+
tmp.append(m)
|
|
303
|
+
if m in reagents:
|
|
304
|
+
reagents.discard(m)
|
|
305
|
+
tmp.extend(reagents)
|
|
306
|
+
reagents = tuple(tmp) if keep_reagents else ()
|
|
307
|
+
|
|
308
|
+
if len(reactants) != len(self.reactants) or len(products) != len(self.products) or len(reagents) != len(self.reagents):
|
|
309
|
+
self._reactants = tuple(reactants)
|
|
310
|
+
self._products = tuple(products)
|
|
311
|
+
self._reagents = reagents
|
|
312
|
+
self.fix_positions()
|
|
313
|
+
return True
|
|
314
|
+
return False
|
|
315
|
+
raise MappingError("Reaction center is absent according to mapping")
|
|
316
|
+
|
|
317
|
+
def contract_ions(self: 'ReactionContainer') -> bool:
|
|
318
|
+
"""
|
|
319
|
+
Contract ions into salts (Molecules with disconnected components).
|
|
320
|
+
Note: works only for unambiguous cases. e.g. equal anions/cations and different or equal cations/anions.
|
|
321
|
+
|
|
322
|
+
Return True if any ions contracted.
|
|
323
|
+
"""
|
|
324
|
+
neutral, cations, anions, total = _sift_ions(self.reagents)
|
|
325
|
+
salts = _contract_ions(anions, cations, total)
|
|
326
|
+
if salts:
|
|
327
|
+
neutral.extend(salts)
|
|
328
|
+
self._reagents = tuple(neutral)
|
|
329
|
+
changed = True
|
|
330
|
+
else:
|
|
331
|
+
changed = False
|
|
332
|
+
|
|
333
|
+
neutral, cations, anions, total = _sift_ions(self.reactants)
|
|
334
|
+
salts = _contract_ions(anions, cations, total)
|
|
335
|
+
if salts:
|
|
336
|
+
anions_order = {frozenset(m): n for n, m in enumerate(anions)}
|
|
337
|
+
cations_order = {frozenset(m): n for n, m in enumerate(cations)}
|
|
338
|
+
neutral.extend(salts)
|
|
339
|
+
self._reactants = tuple(neutral)
|
|
340
|
+
changed = True
|
|
341
|
+
else:
|
|
342
|
+
anions_order = cations_order = {}
|
|
343
|
+
|
|
344
|
+
neutral, cations, anions, total = _sift_ions(self.products)
|
|
345
|
+
if cations and anions:
|
|
346
|
+
anions.sort(key=lambda x: anions_order.get(frozenset(x), -1))
|
|
347
|
+
cations.sort(key=lambda x: cations_order.get(frozenset(x), -1))
|
|
348
|
+
salts = _contract_ions(anions, cations, total)
|
|
349
|
+
if salts:
|
|
350
|
+
neutral.extend(salts)
|
|
351
|
+
self._products = tuple(neutral)
|
|
352
|
+
changed = True
|
|
353
|
+
|
|
354
|
+
if changed:
|
|
355
|
+
self.fix_positions()
|
|
356
|
+
return True
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _sift_ions(mols):
|
|
361
|
+
anions = []
|
|
362
|
+
cations = []
|
|
363
|
+
neutral = []
|
|
364
|
+
total = 0
|
|
365
|
+
for m in mols:
|
|
366
|
+
c = int(m)
|
|
367
|
+
total += c
|
|
368
|
+
if c > 0:
|
|
369
|
+
cations.append(m)
|
|
370
|
+
elif c < 0:
|
|
371
|
+
anions.append(m)
|
|
372
|
+
else:
|
|
373
|
+
neutral.append(m)
|
|
374
|
+
return neutral, cations, anions, total
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _contract_ions(anions, cations, total):
|
|
378
|
+
if not anions or not cations: # nothing to contract
|
|
379
|
+
return
|
|
380
|
+
# check ambiguous cases
|
|
381
|
+
if total > 0:
|
|
382
|
+
if len(cations) > 1: # deficit of anions
|
|
383
|
+
# we have an excess of cations. we can't assign anions univocally
|
|
384
|
+
return # unite is ambiguous
|
|
385
|
+
salt = cations[0]
|
|
386
|
+
shift_x = salt._fix_plane_mean(0) + 1
|
|
387
|
+
for x in anions:
|
|
388
|
+
shift_x = x._fix_plane_mean(shift_x) + 1
|
|
389
|
+
salt = salt | x
|
|
390
|
+
return [salt]
|
|
391
|
+
elif total < 0:
|
|
392
|
+
if len(anions) > 1: # deficit of cations
|
|
393
|
+
# we have an excess of anions. we can't assign cations univocally
|
|
394
|
+
return # unite is ambiguous
|
|
395
|
+
salt = anions[0]
|
|
396
|
+
shift_x = salt._fix_plane_mean(0) + 1
|
|
397
|
+
for x in cations:
|
|
398
|
+
shift_x = x._fix_plane_mean(shift_x) + 1
|
|
399
|
+
salt = salt | x
|
|
400
|
+
return [salt]
|
|
401
|
+
elif len(set(anions)) > 1 and len(set(cations)) > 1: # different anions and cations
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
salts = []
|
|
405
|
+
anions = anions.copy()
|
|
406
|
+
cations = cations.copy()
|
|
407
|
+
while anions:
|
|
408
|
+
ct = cations.pop()
|
|
409
|
+
an = anions.pop()
|
|
410
|
+
shift_x = ct._fix_plane_mean(0) + 1
|
|
411
|
+
shift_x = an._fix_plane_mean(shift_x) + 1
|
|
412
|
+
salt = ct | an
|
|
413
|
+
while True:
|
|
414
|
+
c = int(salt)
|
|
415
|
+
if c > 0:
|
|
416
|
+
an = anions.pop()
|
|
417
|
+
shift_x = an._fix_plane_mean(shift_x) + 1
|
|
418
|
+
salt = salt | an
|
|
419
|
+
elif c < 0:
|
|
420
|
+
ct = cations.pop()
|
|
421
|
+
shift_x = ct._fix_plane_mean(shift_x) + 1
|
|
422
|
+
salt = salt | ct
|
|
423
|
+
else:
|
|
424
|
+
break
|
|
425
|
+
salts.append(salt)
|
|
426
|
+
return salts
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
__all__ = ['StandardizeReaction']
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2021-2024 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 typing import List, TYPE_CHECKING, Union
|
|
20
|
+
from ...exceptions import ValenceError
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from chython import MoleculeContainer
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# atomic number constants
|
|
28
|
+
B = 5
|
|
29
|
+
C = 6
|
|
30
|
+
N = 7
|
|
31
|
+
O = 8
|
|
32
|
+
Si = 14
|
|
33
|
+
P = 15
|
|
34
|
+
S = 16
|
|
35
|
+
As = 33
|
|
36
|
+
Se = 34
|
|
37
|
+
Te = 52
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Resonance:
|
|
41
|
+
__slots__ = ()
|
|
42
|
+
|
|
43
|
+
def fix_resonance(self: Union['MoleculeContainer', 'Resonance'], *, logging=False,
|
|
44
|
+
_fix_stereo=True) -> Union[bool, List[int]]:
|
|
45
|
+
"""
|
|
46
|
+
Transform biradical or dipole resonance structures into neutral form. Return True if structure form changed.
|
|
47
|
+
|
|
48
|
+
:param logging: return list of changed atoms.
|
|
49
|
+
"""
|
|
50
|
+
atoms = self._atoms
|
|
51
|
+
bonds = self._bonds
|
|
52
|
+
entries, exits, rads, constrains, nitrogen_cat, nitrogen_ani, sulfur_cat = self.__entries()
|
|
53
|
+
hs = set()
|
|
54
|
+
while len(rads) > 1:
|
|
55
|
+
n = rads.pop()
|
|
56
|
+
for path in self.__find_delocalize_path(n, rads, constrains, True):
|
|
57
|
+
atoms[n]._is_radical = False
|
|
58
|
+
hs.add(n)
|
|
59
|
+
for n, m, b in path:
|
|
60
|
+
hs.add(m)
|
|
61
|
+
bonds[n][m]._order = b
|
|
62
|
+
atoms[m]._is_radical = False # noqa
|
|
63
|
+
rads.discard(m)
|
|
64
|
+
break # path found
|
|
65
|
+
# path not found. atom n keep as is
|
|
66
|
+
while entries and exits:
|
|
67
|
+
n = entries.pop()
|
|
68
|
+
for path in self.__find_delocalize_path(n, exits, constrains, False):
|
|
69
|
+
l, m, b = path[-1]
|
|
70
|
+
if n in nitrogen_cat and m in nitrogen_ani:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if m in sulfur_cat: # prevent X-[S+]=X >> X=S=X
|
|
74
|
+
if b != 1:
|
|
75
|
+
continue
|
|
76
|
+
atoms[m]._charge -= 1
|
|
77
|
+
else: # check cations end valence.
|
|
78
|
+
atoms[m]._charge -= 1 # reduce atom change and check valence
|
|
79
|
+
try:
|
|
80
|
+
atoms[m].valence_rules(sum(int(y) for x, y in bonds[m].items() if x != l) + b)
|
|
81
|
+
except ValenceError:
|
|
82
|
+
atoms[m]._charge += 1 # roll back
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# succeed!
|
|
86
|
+
atoms[n]._charge += 1
|
|
87
|
+
hs.add(n)
|
|
88
|
+
for n, m, b in path:
|
|
89
|
+
hs.add(m)
|
|
90
|
+
bonds[n][m]._order = b
|
|
91
|
+
exits.discard(m)
|
|
92
|
+
break # path from negative atom to positive atom found.
|
|
93
|
+
# path not found. keep negative atom n as is
|
|
94
|
+
if hs:
|
|
95
|
+
for n in hs:
|
|
96
|
+
self.calc_implicit(n)
|
|
97
|
+
self.flush_cache(keep_sssr=True, keep_components=True)
|
|
98
|
+
if _fix_stereo:
|
|
99
|
+
self.fix_stereo()
|
|
100
|
+
if logging:
|
|
101
|
+
return list(hs)
|
|
102
|
+
return True
|
|
103
|
+
elif logging:
|
|
104
|
+
return []
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
def __find_delocalize_path(self: 'MoleculeContainer', start, finish, constrains, odd_only):
|
|
108
|
+
bonds = self._bonds
|
|
109
|
+
stack = [(start, n, 0, b.order + 1) for n, b in bonds[start].items() if n in constrains and b.order < 3]
|
|
110
|
+
path = []
|
|
111
|
+
seen = {start}
|
|
112
|
+
while stack:
|
|
113
|
+
last, current, depth, order = stack.pop()
|
|
114
|
+
if len(path) > depth:
|
|
115
|
+
seen.difference_update(x for _, x, _ in path[depth:])
|
|
116
|
+
path = path[:depth]
|
|
117
|
+
|
|
118
|
+
path.append((last, current, order))
|
|
119
|
+
|
|
120
|
+
if current in finish:
|
|
121
|
+
if odd_only: # radicals!
|
|
122
|
+
if len(path) % 2:
|
|
123
|
+
yield path
|
|
124
|
+
else: # invalid path
|
|
125
|
+
continue
|
|
126
|
+
elif depth: # one bonded ignored. we search double bond transfer! A=A-A >> A-A=A.
|
|
127
|
+
yield path
|
|
128
|
+
|
|
129
|
+
depth += 1
|
|
130
|
+
seen.add(current)
|
|
131
|
+
diff = -1 if depth % 2 else 1
|
|
132
|
+
stack.extend((current, n, depth, bo) for n, b in bonds[current].items()
|
|
133
|
+
if n not in seen and n in constrains and 1 <= (bo := b.order + diff) <= 3)
|
|
134
|
+
|
|
135
|
+
def __entries(self: 'MoleculeContainer'):
|
|
136
|
+
atoms = self._atoms
|
|
137
|
+
bonds = self._bonds
|
|
138
|
+
errors = {n for n, a in self.atoms() if a.implicit_hydrogens is None}
|
|
139
|
+
|
|
140
|
+
transfer = set()
|
|
141
|
+
entries = set()
|
|
142
|
+
exits = set()
|
|
143
|
+
rads = set()
|
|
144
|
+
nitrogen_cat = set()
|
|
145
|
+
nitrogen_ani = set()
|
|
146
|
+
sulfur_cat = set()
|
|
147
|
+
for n, a in self.atoms():
|
|
148
|
+
if a not in (B, C, N, O, Si, P, S, As, Se, Te):
|
|
149
|
+
# filter non-organic set, halogens and aromatics
|
|
150
|
+
continue
|
|
151
|
+
elif a.is_radical:
|
|
152
|
+
rads.add(n)
|
|
153
|
+
elif a.charge == -1:
|
|
154
|
+
if (lb := len(bonds[n])) == 4 and a == B: # skip boron
|
|
155
|
+
continue
|
|
156
|
+
elif lb == 6 and a == P: # skip [P-]X6
|
|
157
|
+
continue
|
|
158
|
+
if n in errors: # only valid anions accepted
|
|
159
|
+
continue
|
|
160
|
+
entries.add(n)
|
|
161
|
+
elif a.charge == 1:
|
|
162
|
+
lb = len(bonds[n])
|
|
163
|
+
if a == N:
|
|
164
|
+
if lb == 4: # skip ammonia
|
|
165
|
+
continue
|
|
166
|
+
elif lb == 2 and a.hybridization == 3: # skip Azide
|
|
167
|
+
(n1, b1), (n2, b2) = bonds[n].items()
|
|
168
|
+
an1 = atoms[n1]
|
|
169
|
+
an2 = atoms[n2]
|
|
170
|
+
if b1 == b2 == 2 and (an1.charge == -1 and an1 == N or an2.charge == -1 and an2 == N):
|
|
171
|
+
continue
|
|
172
|
+
elif lb == 3 and a.hybridization == 2: # X=[N+](-X)-X - prevent N-N migration
|
|
173
|
+
nitrogen_ani.add(n)
|
|
174
|
+
elif a == P and lb == 4: # skip [P+]R4
|
|
175
|
+
continue
|
|
176
|
+
elif a == S:
|
|
177
|
+
if lb == 2 and a.hybridization == 2: # ad-hoc for X-[S+]=X
|
|
178
|
+
sulfur_cat.add(n)
|
|
179
|
+
elif lb == 3 and a.hybridization == 1: # ad-hoc for X-[S+](-X)-X
|
|
180
|
+
continue
|
|
181
|
+
exits.add(n)
|
|
182
|
+
transfer.add(n)
|
|
183
|
+
|
|
184
|
+
if exits or entries: # try to move cation to nitrogen. saturation fixup.
|
|
185
|
+
for n, a in self.atoms():
|
|
186
|
+
if a == N and not a.charge:
|
|
187
|
+
if a.hybridization == 1 and a.neighbors <= 3: # any amine - potential e-donor
|
|
188
|
+
entries.add(n)
|
|
189
|
+
nitrogen_cat.add(n)
|
|
190
|
+
elif a.hybridization == 3 and a.neighbors == 1: # N#X-[X-] >> [N-]=X=X
|
|
191
|
+
exits.add(n)
|
|
192
|
+
nitrogen_ani.add(n)
|
|
193
|
+
return entries, exits, rads, transfer, nitrogen_cat, nitrogen_ani, sulfur_cat
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
__all__ = ['Resonance']
|