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,384 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2018-2026 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 ._reagents import *
|
|
23
|
+
from ...exceptions import MappingError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StandardizeReaction:
|
|
27
|
+
__slots__ = ()
|
|
28
|
+
|
|
29
|
+
def canonicalize(self, *, fix_mapping: bool = True, logging=False, fix_tautomers=True) -> bool | list:
|
|
30
|
+
"""
|
|
31
|
+
Convert molecules to canonical forms of functional groups and aromatic rings without explicit hydrogens.
|
|
32
|
+
Return True if in any molecule found not canonical group.
|
|
33
|
+
|
|
34
|
+
:param fix_mapping: Search AAM errors of functional groups.
|
|
35
|
+
:param logging: return processing log.
|
|
36
|
+
:param fix_tautomers: convert tautomers to canonical forms.
|
|
37
|
+
"""
|
|
38
|
+
total = []
|
|
39
|
+
for n, m in enumerate(self.molecules()):
|
|
40
|
+
total.extend((n, x) for x in m.canonicalize(logging=True, fix_tautomers=fix_tautomers))
|
|
41
|
+
|
|
42
|
+
if fix_mapping:
|
|
43
|
+
total.extend(self.fix_mapping(logging=True))
|
|
44
|
+
|
|
45
|
+
if total:
|
|
46
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
47
|
+
if logging:
|
|
48
|
+
return total
|
|
49
|
+
return bool(total)
|
|
50
|
+
|
|
51
|
+
def thiele(self, *, fix_tautomers=True) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Convert structures to aromatic form.
|
|
54
|
+
Return True if in any molecule found kekule ring
|
|
55
|
+
|
|
56
|
+
:param fix_tautomers: convert tautomers to canonical forms.
|
|
57
|
+
"""
|
|
58
|
+
total = False
|
|
59
|
+
for m in self.molecules():
|
|
60
|
+
if m.thiele(fix_tautomers=fix_tautomers):
|
|
61
|
+
total = True
|
|
62
|
+
if total:
|
|
63
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
64
|
+
return total
|
|
65
|
+
|
|
66
|
+
def kekule(self, *, buffer_size=7, ignore_pyrrole_hydrogen=False) -> bool:
|
|
67
|
+
"""
|
|
68
|
+
Convert structures to a kekule form.
|
|
69
|
+
Return True if in any molecule found aromatic ring
|
|
70
|
+
|
|
71
|
+
:param buffer_size: number of attempts of pyridine form searching.
|
|
72
|
+
:param ignore_pyrrole_hydrogen: ignore hydrogen on pyrrole to fix invalid rings like Cn1cc[nH]c1.
|
|
73
|
+
"""
|
|
74
|
+
total = False
|
|
75
|
+
for m in self.molecules():
|
|
76
|
+
if m.kekule(buffer_size=buffer_size, ignore_pyrrole_hydrogen=ignore_pyrrole_hydrogen):
|
|
77
|
+
total = True
|
|
78
|
+
if total:
|
|
79
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
80
|
+
return total
|
|
81
|
+
|
|
82
|
+
def clean_isotopes(self) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Clean isotope marks for all molecules in reaction.
|
|
85
|
+
Returns True if in any molecule found isotope.
|
|
86
|
+
"""
|
|
87
|
+
flag = False
|
|
88
|
+
for m in self.molecules():
|
|
89
|
+
if m.clean_isotopes():
|
|
90
|
+
flag = True
|
|
91
|
+
if flag:
|
|
92
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
93
|
+
return flag
|
|
94
|
+
|
|
95
|
+
def clean_stereo(self):
|
|
96
|
+
"""
|
|
97
|
+
Remove stereo data
|
|
98
|
+
"""
|
|
99
|
+
for m in self.molecules():
|
|
100
|
+
m.clean_stereo()
|
|
101
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
102
|
+
|
|
103
|
+
def implicify_hydrogens(self) -> int:
|
|
104
|
+
"""
|
|
105
|
+
Remove explicit hydrogens if possible.
|
|
106
|
+
|
|
107
|
+
:return: number of removed hydrogens.
|
|
108
|
+
"""
|
|
109
|
+
total = 0
|
|
110
|
+
for m in self.molecules():
|
|
111
|
+
total += m.implicify_hydrogens()
|
|
112
|
+
if total:
|
|
113
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
114
|
+
return total
|
|
115
|
+
|
|
116
|
+
def explicify_hydrogens(self) -> int:
|
|
117
|
+
"""
|
|
118
|
+
Add explicit hydrogens to atoms
|
|
119
|
+
|
|
120
|
+
:return: number of added atoms
|
|
121
|
+
"""
|
|
122
|
+
total = 0
|
|
123
|
+
start_map = 0
|
|
124
|
+
for m in self.molecules():
|
|
125
|
+
map_ = max(m, default=0)
|
|
126
|
+
if map_ > start_map:
|
|
127
|
+
start_map = map_
|
|
128
|
+
|
|
129
|
+
mapping = defaultdict(list)
|
|
130
|
+
for m in self.reactants:
|
|
131
|
+
maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
|
|
132
|
+
if maps:
|
|
133
|
+
for n, h in maps:
|
|
134
|
+
mapping[n].append(h)
|
|
135
|
+
start_map = maps[-1][1]
|
|
136
|
+
total += len(maps)
|
|
137
|
+
|
|
138
|
+
for m in self.reagents:
|
|
139
|
+
maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
|
|
140
|
+
if maps:
|
|
141
|
+
start_map = maps[-1][1]
|
|
142
|
+
total += len(maps)
|
|
143
|
+
|
|
144
|
+
for m in self.products:
|
|
145
|
+
maps = m.explicify_hydrogens(_return_map=True, start_map=start_map + 1)
|
|
146
|
+
if maps:
|
|
147
|
+
total += len(maps)
|
|
148
|
+
remap = {}
|
|
149
|
+
free = []
|
|
150
|
+
for n, h in maps:
|
|
151
|
+
if n in mapping and mapping[n]:
|
|
152
|
+
remap[h] = mapping[n].pop()
|
|
153
|
+
free.append(h)
|
|
154
|
+
elif free:
|
|
155
|
+
remap[h] = start_map = free.pop(0)
|
|
156
|
+
else:
|
|
157
|
+
start_map = h
|
|
158
|
+
m.remap(remap)
|
|
159
|
+
|
|
160
|
+
if total:
|
|
161
|
+
self.flush_cache(keep_molecule_cache=True)
|
|
162
|
+
return total
|
|
163
|
+
|
|
164
|
+
def remove_reagents(self, *, keep_reagents: bool = False, mapping: bool = True) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Place molecules, except reactants, to reagents list. Reagents - molecules which atoms not presented in products.
|
|
167
|
+
Mapping based approach remove molecules without reaction center.
|
|
168
|
+
Rule based approach remove equal molecules in reactants and products, and predefined reactants.
|
|
169
|
+
|
|
170
|
+
:param mapping: use atom-to-atom mapping to detect reagents, otherwise use predefined list of common reagents.
|
|
171
|
+
:param keep_reagents: delete reagents if False
|
|
172
|
+
|
|
173
|
+
Return True if any reagent found.
|
|
174
|
+
"""
|
|
175
|
+
if mapping:
|
|
176
|
+
return self.__remove_reagents_mapping(keep_reagents)
|
|
177
|
+
return self.__remove_reagents_rules(keep_reagents)
|
|
178
|
+
|
|
179
|
+
def __remove_reagents_rules(self, keep_reagents):
|
|
180
|
+
if not self.reactants or not self.products: # there is no reaction
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
reactants_st1 = []
|
|
184
|
+
products_st1 = []
|
|
185
|
+
reagents_st1 = set(self.reagents)
|
|
186
|
+
|
|
187
|
+
# find the same molecules in reactants and products
|
|
188
|
+
for m in self.reactants:
|
|
189
|
+
if m in self.products:
|
|
190
|
+
reagents_st1.add(m)
|
|
191
|
+
else:
|
|
192
|
+
reactants_st1.append(m)
|
|
193
|
+
for m in self.products:
|
|
194
|
+
if m in self.reactants:
|
|
195
|
+
reagents_st1.add(m)
|
|
196
|
+
else:
|
|
197
|
+
products_st1.append(m)
|
|
198
|
+
if not reactants_st1 or not products_st1:
|
|
199
|
+
return False # keep bad reaction as is
|
|
200
|
+
|
|
201
|
+
reactants_st2 = []
|
|
202
|
+
products_st2 = []
|
|
203
|
+
reagents_st2 = reagents_st1.copy()
|
|
204
|
+
|
|
205
|
+
# filter out predefined reagents
|
|
206
|
+
for m in reactants_st1:
|
|
207
|
+
if m in reagents_set:
|
|
208
|
+
reagents_st2.add(m)
|
|
209
|
+
else:
|
|
210
|
+
reactants_st2.append(m)
|
|
211
|
+
for m in products_st1:
|
|
212
|
+
if m in reagents_set:
|
|
213
|
+
reagents_st2.add(m)
|
|
214
|
+
else:
|
|
215
|
+
products_st2.append(m)
|
|
216
|
+
if not reactants_st2 or not products_st2: # reaction contains only simple molecules. roll-back to step 1
|
|
217
|
+
reactants_st2 = reactants_st1
|
|
218
|
+
products_st2 = products_st1
|
|
219
|
+
reagents_st2 = reagents_st1
|
|
220
|
+
|
|
221
|
+
# remove reagents from reactants
|
|
222
|
+
tmp = []
|
|
223
|
+
for m in self.reagents:
|
|
224
|
+
tmp.append(m)
|
|
225
|
+
if m in reagents_st2:
|
|
226
|
+
reagents_st2.discard(m)
|
|
227
|
+
tmp.extend(reagents_st2)
|
|
228
|
+
reagents = tuple(tmp) if keep_reagents else ()
|
|
229
|
+
|
|
230
|
+
self._reactants = tuple(reactants_st2)
|
|
231
|
+
self._products = tuple(products_st2)
|
|
232
|
+
self._reagents = reagents
|
|
233
|
+
self.fix_positions()
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
def __remove_reagents_mapping(self, keep_reagents):
|
|
237
|
+
cgr = ~self
|
|
238
|
+
if cgr.center_atoms:
|
|
239
|
+
active = set(cgr.center_atoms)
|
|
240
|
+
reactants = []
|
|
241
|
+
products = []
|
|
242
|
+
reagents = set(self.reagents)
|
|
243
|
+
for i in self.reactants:
|
|
244
|
+
if not active.isdisjoint(i):
|
|
245
|
+
reactants.append(i)
|
|
246
|
+
else:
|
|
247
|
+
reagents.add(i)
|
|
248
|
+
for i in self.products:
|
|
249
|
+
if not active.isdisjoint(i):
|
|
250
|
+
products.append(i)
|
|
251
|
+
else:
|
|
252
|
+
reagents.add(i)
|
|
253
|
+
|
|
254
|
+
# remove reagents from reactants
|
|
255
|
+
tmp = []
|
|
256
|
+
for m in self.reagents:
|
|
257
|
+
tmp.append(m)
|
|
258
|
+
if m in reagents:
|
|
259
|
+
reagents.discard(m)
|
|
260
|
+
tmp.extend(reagents)
|
|
261
|
+
reagents = tuple(tmp) if keep_reagents else ()
|
|
262
|
+
|
|
263
|
+
if len(reactants) != len(self.reactants) or len(products) != len(self.products) or len(reagents) != len(self.reagents):
|
|
264
|
+
self._reactants = tuple(reactants)
|
|
265
|
+
self._products = tuple(products)
|
|
266
|
+
self._reagents = reagents
|
|
267
|
+
self.fix_positions()
|
|
268
|
+
return True
|
|
269
|
+
return False
|
|
270
|
+
raise MappingError("Reaction center is absent according to mapping")
|
|
271
|
+
|
|
272
|
+
def contract_ions(self) -> bool:
|
|
273
|
+
"""
|
|
274
|
+
Contract ions into salts (Molecules with disconnected components).
|
|
275
|
+
Note: works only for unambiguous cases. e.g. equal anions/cations and different or equal cations/anions.
|
|
276
|
+
|
|
277
|
+
Return True if any ions contracted.
|
|
278
|
+
"""
|
|
279
|
+
neutral, cations, anions, total = _sift_ions(self.reagents)
|
|
280
|
+
salts = _contract_ions(anions, cations, total)
|
|
281
|
+
if salts:
|
|
282
|
+
neutral.extend(salts)
|
|
283
|
+
self._reagents = tuple(neutral)
|
|
284
|
+
changed = True
|
|
285
|
+
else:
|
|
286
|
+
changed = False
|
|
287
|
+
|
|
288
|
+
neutral, cations, anions, total = _sift_ions(self.reactants)
|
|
289
|
+
salts = _contract_ions(anions, cations, total)
|
|
290
|
+
if salts:
|
|
291
|
+
anions_order = {frozenset(m): n for n, m in enumerate(anions)}
|
|
292
|
+
cations_order = {frozenset(m): n for n, m in enumerate(cations)}
|
|
293
|
+
neutral.extend(salts)
|
|
294
|
+
self._reactants = tuple(neutral)
|
|
295
|
+
changed = True
|
|
296
|
+
else:
|
|
297
|
+
anions_order = cations_order = {}
|
|
298
|
+
|
|
299
|
+
neutral, cations, anions, total = _sift_ions(self.products)
|
|
300
|
+
if cations and anions:
|
|
301
|
+
anions.sort(key=lambda x: anions_order.get(frozenset(x), -1))
|
|
302
|
+
cations.sort(key=lambda x: cations_order.get(frozenset(x), -1))
|
|
303
|
+
salts = _contract_ions(anions, cations, total)
|
|
304
|
+
if salts:
|
|
305
|
+
neutral.extend(salts)
|
|
306
|
+
self._products = tuple(neutral)
|
|
307
|
+
changed = True
|
|
308
|
+
|
|
309
|
+
if changed:
|
|
310
|
+
self.fix_positions()
|
|
311
|
+
return True
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _sift_ions(mols):
|
|
316
|
+
anions = []
|
|
317
|
+
cations = []
|
|
318
|
+
neutral = []
|
|
319
|
+
total = 0
|
|
320
|
+
for m in mols:
|
|
321
|
+
c = int(m)
|
|
322
|
+
total += c
|
|
323
|
+
if c > 0:
|
|
324
|
+
cations.append(m)
|
|
325
|
+
elif c < 0:
|
|
326
|
+
anions.append(m)
|
|
327
|
+
else:
|
|
328
|
+
neutral.append(m)
|
|
329
|
+
return neutral, cations, anions, total
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _contract_ions(anions, cations, total):
|
|
333
|
+
if not anions or not cations: # nothing to contract
|
|
334
|
+
return
|
|
335
|
+
# check ambiguous cases
|
|
336
|
+
if total > 0:
|
|
337
|
+
if len(cations) > 1: # deficit of anions
|
|
338
|
+
# we have an excess of cations. we can't assign anions univocally
|
|
339
|
+
return # unite is ambiguous
|
|
340
|
+
salt = cations[0]
|
|
341
|
+
shift_x = salt._fix_plane_mean(0) + 1
|
|
342
|
+
for x in anions:
|
|
343
|
+
shift_x = x._fix_plane_mean(shift_x) + 1
|
|
344
|
+
salt = salt | x
|
|
345
|
+
return [salt]
|
|
346
|
+
elif total < 0:
|
|
347
|
+
if len(anions) > 1: # deficit of cations
|
|
348
|
+
# we have an excess of anions. we can't assign cations univocally
|
|
349
|
+
return # unite is ambiguous
|
|
350
|
+
salt = anions[0]
|
|
351
|
+
shift_x = salt._fix_plane_mean(0) + 1
|
|
352
|
+
for x in cations:
|
|
353
|
+
shift_x = x._fix_plane_mean(shift_x) + 1
|
|
354
|
+
salt = salt | x
|
|
355
|
+
return [salt]
|
|
356
|
+
elif len(set(anions)) > 1 and len(set(cations)) > 1: # different anions and cations
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
salts = []
|
|
360
|
+
anions = anions.copy()
|
|
361
|
+
cations = cations.copy()
|
|
362
|
+
while anions:
|
|
363
|
+
ct = cations.pop()
|
|
364
|
+
an = anions.pop()
|
|
365
|
+
shift_x = ct._fix_plane_mean(0) + 1
|
|
366
|
+
shift_x = an._fix_plane_mean(shift_x) + 1
|
|
367
|
+
salt = ct | an
|
|
368
|
+
while True:
|
|
369
|
+
c = int(salt)
|
|
370
|
+
if c > 0:
|
|
371
|
+
an = anions.pop()
|
|
372
|
+
shift_x = an._fix_plane_mean(shift_x) + 1
|
|
373
|
+
salt = salt | an
|
|
374
|
+
elif c < 0:
|
|
375
|
+
ct = cations.pop()
|
|
376
|
+
shift_x = ct._fix_plane_mean(shift_x) + 1
|
|
377
|
+
salt = salt | ct
|
|
378
|
+
else:
|
|
379
|
+
break
|
|
380
|
+
salts.append(salt)
|
|
381
|
+
return salts
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
__all__ = ['StandardizeReaction']
|
|
@@ -0,0 +1,191 @@
|
|
|
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 ...exceptions import ValenceError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# atomic number constants
|
|
23
|
+
B = 5
|
|
24
|
+
C = 6
|
|
25
|
+
N = 7
|
|
26
|
+
O = 8
|
|
27
|
+
Si = 14
|
|
28
|
+
P = 15
|
|
29
|
+
S = 16
|
|
30
|
+
As = 33
|
|
31
|
+
Se = 34
|
|
32
|
+
Te = 52
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Resonance:
|
|
36
|
+
__slots__ = ()
|
|
37
|
+
|
|
38
|
+
def fix_resonance(self, *, logging=False) -> bool | list:
|
|
39
|
+
"""
|
|
40
|
+
Transform biradical or dipole resonance structures into neutral form. Return True if structure form changed.
|
|
41
|
+
|
|
42
|
+
:param logging: return list of changed atoms.
|
|
43
|
+
"""
|
|
44
|
+
atoms = self._atoms
|
|
45
|
+
bonds = self._bonds
|
|
46
|
+
entries, exits, rads, constrains, nitrogen_cat, nitrogen_ani, sulfur_cat = self.__entries()
|
|
47
|
+
hs = set()
|
|
48
|
+
while len(rads) > 1:
|
|
49
|
+
n = rads.pop()
|
|
50
|
+
for path in self.__find_delocalize_path(n, rads, constrains, True):
|
|
51
|
+
atoms[n]._is_radical = False
|
|
52
|
+
hs.add(n)
|
|
53
|
+
for n, m, b in path:
|
|
54
|
+
hs.add(m)
|
|
55
|
+
bonds[n][m]._order = b
|
|
56
|
+
atoms[m]._is_radical = False # noqa
|
|
57
|
+
rads.discard(m)
|
|
58
|
+
break # path found
|
|
59
|
+
# path not found. atom n keep as is
|
|
60
|
+
while entries and exits:
|
|
61
|
+
n = entries.pop()
|
|
62
|
+
for path in self.__find_delocalize_path(n, exits, constrains, False):
|
|
63
|
+
l, m, b = path[-1]
|
|
64
|
+
if n in nitrogen_cat and m in nitrogen_ani:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
if m in sulfur_cat: # prevent X-[S+]=X >> X=S=X
|
|
68
|
+
if b != 1:
|
|
69
|
+
continue
|
|
70
|
+
atoms[m]._charge -= 1
|
|
71
|
+
else: # check cations end valence.
|
|
72
|
+
atoms[m]._charge -= 1 # reduce atom change and check valence
|
|
73
|
+
try:
|
|
74
|
+
atoms[m].valence_rules(sum(int(y) for x, y in bonds[m].items() if x != l) + b)
|
|
75
|
+
except ValenceError:
|
|
76
|
+
atoms[m]._charge += 1 # roll back
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# succeed!
|
|
80
|
+
atoms[n]._charge += 1
|
|
81
|
+
hs.add(n)
|
|
82
|
+
for n, m, b in path:
|
|
83
|
+
hs.add(m)
|
|
84
|
+
bonds[n][m]._order = b
|
|
85
|
+
exits.discard(m)
|
|
86
|
+
break # path from negative atom to positive atom found.
|
|
87
|
+
# path not found. keep negative atom n as is
|
|
88
|
+
if hs:
|
|
89
|
+
for n in hs:
|
|
90
|
+
self.calc_implicit(n)
|
|
91
|
+
self.flush_cache(keep_sssr=True, keep_components=True, keep_special_connectivity=True)
|
|
92
|
+
self.fix_stereo()
|
|
93
|
+
if logging:
|
|
94
|
+
return list(hs)
|
|
95
|
+
return True
|
|
96
|
+
elif logging:
|
|
97
|
+
return []
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def __find_delocalize_path(self, start, finish, constrains, odd_only):
|
|
101
|
+
bonds = self._bonds
|
|
102
|
+
stack = [(start, n, 0, b.order + 1) for n, b in bonds[start].items() if n in constrains and b.order < 3]
|
|
103
|
+
path = []
|
|
104
|
+
seen = {start}
|
|
105
|
+
while stack:
|
|
106
|
+
last, current, depth, order = stack.pop()
|
|
107
|
+
if len(path) > depth:
|
|
108
|
+
seen.difference_update(x for _, x, _ in path[depth:])
|
|
109
|
+
path = path[:depth]
|
|
110
|
+
|
|
111
|
+
path.append((last, current, order))
|
|
112
|
+
|
|
113
|
+
if current in finish:
|
|
114
|
+
if odd_only: # radicals!
|
|
115
|
+
if len(path) % 2:
|
|
116
|
+
yield path
|
|
117
|
+
else: # invalid path
|
|
118
|
+
continue
|
|
119
|
+
elif depth: # one bonded ignored. we search double bond transfer! A=A-A >> A-A=A.
|
|
120
|
+
yield path
|
|
121
|
+
|
|
122
|
+
depth += 1
|
|
123
|
+
seen.add(current)
|
|
124
|
+
diff = -1 if depth % 2 else 1
|
|
125
|
+
stack.extend((current, n, depth, bo) for n, b in bonds[current].items()
|
|
126
|
+
if n not in seen and n in constrains and 1 <= (bo := b.order + diff) <= 3)
|
|
127
|
+
|
|
128
|
+
def __entries(self):
|
|
129
|
+
atoms = self._atoms
|
|
130
|
+
bonds = self._bonds
|
|
131
|
+
errors = {n for n, a in self.atoms() if a.implicit_hydrogens is None}
|
|
132
|
+
|
|
133
|
+
transfer = set()
|
|
134
|
+
entries = set()
|
|
135
|
+
exits = set()
|
|
136
|
+
rads = set()
|
|
137
|
+
nitrogen_cat = set()
|
|
138
|
+
nitrogen_ani = set()
|
|
139
|
+
sulfur_cat = set()
|
|
140
|
+
for n, a in self.atoms():
|
|
141
|
+
if a not in (B, C, N, O, Si, P, S, As, Se, Te):
|
|
142
|
+
# filter non-organic set, halogens and aromatics
|
|
143
|
+
continue
|
|
144
|
+
elif a.is_radical:
|
|
145
|
+
rads.add(n)
|
|
146
|
+
elif a.charge == -1:
|
|
147
|
+
if a == B and a.total_hydrogens: # skip [BHx-]
|
|
148
|
+
continue
|
|
149
|
+
elif (lb := len(bonds[n])) == 4 and a == B: # skip [B-]X4
|
|
150
|
+
continue
|
|
151
|
+
elif lb == 6 and a == P: # skip [P-]X6
|
|
152
|
+
continue
|
|
153
|
+
if n in errors: # only valid anions accepted
|
|
154
|
+
continue
|
|
155
|
+
entries.add(n)
|
|
156
|
+
elif a.charge == 1:
|
|
157
|
+
lb = len(bonds[n])
|
|
158
|
+
if a == N:
|
|
159
|
+
if lb == 4: # skip ammonia
|
|
160
|
+
continue
|
|
161
|
+
elif lb == 2 and a.hybridization == 3: # skip Azide
|
|
162
|
+
(n1, b1), (n2, b2) = bonds[n].items()
|
|
163
|
+
an1 = atoms[n1]
|
|
164
|
+
an2 = atoms[n2]
|
|
165
|
+
if b1 == b2 == 2 and (an1.charge == -1 and an1 == N or an2.charge == -1 and an2 == N):
|
|
166
|
+
continue
|
|
167
|
+
elif lb == 3 and a.hybridization == 2: # X=[N+](-X)-X - prevent N-N migration
|
|
168
|
+
nitrogen_ani.add(n)
|
|
169
|
+
elif a == P and lb == 4: # skip [P+]R4
|
|
170
|
+
continue
|
|
171
|
+
elif a == S:
|
|
172
|
+
if lb == 2 and a.hybridization == 2: # ad-hoc for X-[S+]=X
|
|
173
|
+
sulfur_cat.add(n)
|
|
174
|
+
elif lb == 3 and a.hybridization == 1: # ad-hoc for X-[S+](-X)-X
|
|
175
|
+
continue
|
|
176
|
+
exits.add(n)
|
|
177
|
+
transfer.add(n)
|
|
178
|
+
|
|
179
|
+
if exits or entries: # try to move cation to nitrogen. saturation fixup.
|
|
180
|
+
for n, a in self.atoms():
|
|
181
|
+
if a == N and not a.charge:
|
|
182
|
+
if a.hybridization == 1 and a.neighbors <= 3: # any amine - potential e-donor
|
|
183
|
+
entries.add(n)
|
|
184
|
+
nitrogen_cat.add(n)
|
|
185
|
+
elif a.hybridization == 3 and a.neighbors == 1: # N#X-[X-] >> [N-]=X=X
|
|
186
|
+
exits.add(n)
|
|
187
|
+
nitrogen_ani.add(n)
|
|
188
|
+
return entries, exits, rads, transfer, nitrogen_cat, nitrogen_ani, sulfur_cat
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
__all__ = ['Resonance']
|
|
@@ -0,0 +1,128 @@
|
|
|
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 ._salts import acids, rules
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# atomic number constants
|
|
23
|
+
H = 1
|
|
24
|
+
N = 7
|
|
25
|
+
s_metals = {3, 4, 11, 12, 19, 20, 37, 38, 55, 56, 87, 88}
|
|
26
|
+
s_metals_ammonia = {3, 4, 7, 11, 12, 19, 20, 37, 38, 55, 56, 87, 88}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Salts:
|
|
30
|
+
__slots__ = ()
|
|
31
|
+
|
|
32
|
+
def remove_metals(self, *, skip_elements: list[int] = None, logging=False) -> bool | list:
|
|
33
|
+
"""
|
|
34
|
+
Remove disconnected S-metals and ammonia.
|
|
35
|
+
|
|
36
|
+
:param logging: return deleted atoms list.
|
|
37
|
+
:param skip_elements: skip elements from removing.
|
|
38
|
+
"""
|
|
39
|
+
atoms = self._atoms
|
|
40
|
+
bonds = self._bonds
|
|
41
|
+
filters = s_metals_ammonia.difference(skip_elements) if skip_elements else s_metals_ammonia
|
|
42
|
+
|
|
43
|
+
metals = []
|
|
44
|
+
for n, a in atoms.items():
|
|
45
|
+
if not bonds[n] and a.atomic_number in filters:
|
|
46
|
+
metals.append(n)
|
|
47
|
+
|
|
48
|
+
if 0 < len(metals) < len(self):
|
|
49
|
+
for n in metals:
|
|
50
|
+
del atoms[n]
|
|
51
|
+
del bonds[n]
|
|
52
|
+
|
|
53
|
+
self.flush_cache(keep_sssr=True)
|
|
54
|
+
if logging:
|
|
55
|
+
return metals
|
|
56
|
+
return True
|
|
57
|
+
elif logging:
|
|
58
|
+
return []
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def remove_acids(self, *, logging=False) -> bool | list:
|
|
62
|
+
"""
|
|
63
|
+
Remove common acids from organic bases salts.
|
|
64
|
+
Works only for neutral pairs like HA+B. Use `neutralize` before.
|
|
65
|
+
|
|
66
|
+
:param logging: return deleted atoms list.
|
|
67
|
+
"""
|
|
68
|
+
if self.connected_components_count > 1:
|
|
69
|
+
log = []
|
|
70
|
+
for c in self.connected_components:
|
|
71
|
+
if self.substructure(c, recalculate_hydrogens=False) in acids:
|
|
72
|
+
log.extend(c)
|
|
73
|
+
if 0 < len(log) < len(self): # prevent singularity
|
|
74
|
+
atoms = self._atoms
|
|
75
|
+
bonds = self._bonds
|
|
76
|
+
|
|
77
|
+
for n in log:
|
|
78
|
+
del atoms[n]
|
|
79
|
+
del bonds[n]
|
|
80
|
+
|
|
81
|
+
self.flush_cache()
|
|
82
|
+
if logging:
|
|
83
|
+
return log
|
|
84
|
+
return True
|
|
85
|
+
if logging:
|
|
86
|
+
return []
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def split_metal_salts(self, *, skip_elements: list[int] = None, logging=False) -> bool | list:
|
|
90
|
+
"""
|
|
91
|
+
Split connected S-metal salts to cation/anion pairs.
|
|
92
|
+
|
|
93
|
+
:param logging: return deleted bonds list.
|
|
94
|
+
:param skip_elements: skip elements from splitting.
|
|
95
|
+
"""
|
|
96
|
+
atoms = self._atoms
|
|
97
|
+
bonds = self._bonds
|
|
98
|
+
filters = s_metals.difference(skip_elements) if skip_elements else s_metals
|
|
99
|
+
|
|
100
|
+
metals = [n for n, a in atoms.items() if a.atomic_number in filters]
|
|
101
|
+
if metals:
|
|
102
|
+
acceptors = set()
|
|
103
|
+
log = []
|
|
104
|
+
for q in rules:
|
|
105
|
+
for mapping in q.get_mapping(self, automorphism_filter=False):
|
|
106
|
+
acceptors.add(mapping[1])
|
|
107
|
+
|
|
108
|
+
for n in metals:
|
|
109
|
+
for m in acceptors & bonds[n].keys():
|
|
110
|
+
if atoms[n].charge == 4: # prevent overcharging
|
|
111
|
+
break
|
|
112
|
+
del bonds[n][m]
|
|
113
|
+
del bonds[m][n]
|
|
114
|
+
atoms[n]._charge += 1
|
|
115
|
+
atoms[m]._charge -= 1
|
|
116
|
+
log.append((n, m))
|
|
117
|
+
if log:
|
|
118
|
+
self.flush_cache()
|
|
119
|
+
self.fix_stereo()
|
|
120
|
+
if logging:
|
|
121
|
+
return log
|
|
122
|
+
return True
|
|
123
|
+
if logging:
|
|
124
|
+
return []
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
__all__ = ['Salts']
|