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,475 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2018-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2021 Dmitrij Zanadvornykh <zandmitrij@gmail.com>
|
|
5
|
+
# Copyright 2018 Tagir Akhmetshin <tagirshin@gmail.com>
|
|
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 ._groups import rules as fragment_rules
|
|
23
|
+
from ._isomers import rules as isomer_rules
|
|
24
|
+
from ._metal_organics import rules as metal_rules
|
|
25
|
+
from ...containers.bonds import Bond
|
|
26
|
+
from ...exceptions import ValenceError, ImplementationError
|
|
27
|
+
from ...periodictable import H as _H
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
H = 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Standardize:
|
|
34
|
+
__slots__ = ()
|
|
35
|
+
|
|
36
|
+
def canonicalize(self, *, fix_tautomers=True, keep_kekule=False,
|
|
37
|
+
ignore_pyrrole_hydrogen=False, buffer_size=7,
|
|
38
|
+
logging=False, ignore=True) -> bool | list:
|
|
39
|
+
"""
|
|
40
|
+
Convert molecule to canonical forms of functional groups and aromatic rings without explicit hydrogens.
|
|
41
|
+
|
|
42
|
+
:param logging: return log.
|
|
43
|
+
:param ignore: ignore standardization bugs.
|
|
44
|
+
:param fix_tautomers: convert tautomers to canonical forms.
|
|
45
|
+
:param keep_kekule: return kekule form.
|
|
46
|
+
:param buffer_size: number of attempts of pyridine form searching.
|
|
47
|
+
:param ignore_pyrrole_hydrogen: ignore hydrogen on pyrrole to fix invalid rings like Cn1cc[nH]c1.
|
|
48
|
+
"""
|
|
49
|
+
k = self.kekule(buffer_size=buffer_size, ignore_pyrrole_hydrogen=ignore_pyrrole_hydrogen)
|
|
50
|
+
s = self.standardize(logging=True, ignore=ignore, fix_tautomers=fix_tautomers)
|
|
51
|
+
h, changed = self.implicify_hydrogens(logging=True)
|
|
52
|
+
|
|
53
|
+
if logging or keep_kekule: # thiele can change tautomeric form
|
|
54
|
+
hgs = {n: a.implicit_hydrogens for n, a in self.atoms()}
|
|
55
|
+
if keep_kekule: # save bond orders
|
|
56
|
+
bonds = [(b, b.order) for _, _, b in self.bonds()]
|
|
57
|
+
|
|
58
|
+
t = self.thiele(fix_tautomers=fix_tautomers)
|
|
59
|
+
a = self.standardize_isomers(logging=True)
|
|
60
|
+
|
|
61
|
+
if keep_kekule and t: # restore
|
|
62
|
+
# check ring charge/hydrogen moving
|
|
63
|
+
if a or any(hgs[n] != aa.implicit_hydrogens for n, aa in self.atoms()):
|
|
64
|
+
self.kekule() # we need to do full kekule again
|
|
65
|
+
else:
|
|
66
|
+
for b, o in bonds: # noqa
|
|
67
|
+
b._order = o
|
|
68
|
+
self.flush_cache()
|
|
69
|
+
self.calc_labels()
|
|
70
|
+
|
|
71
|
+
if logging:
|
|
72
|
+
if k:
|
|
73
|
+
s.insert(0, ((), -1, 'kekulized'))
|
|
74
|
+
if h:
|
|
75
|
+
s.append((tuple(changed), -1, 'implicified'))
|
|
76
|
+
if t:
|
|
77
|
+
s.append(((), -1, 'aromatized'))
|
|
78
|
+
if fix_tautomers and (x := tuple(n for n, aa in self.atoms() if hgs[n] != aa.implicit_hydrogens)):
|
|
79
|
+
s.append((x, -1, 'aromatic tautomer found'))
|
|
80
|
+
if a:
|
|
81
|
+
s.append((tuple(a), -1, 'isomers standardized'))
|
|
82
|
+
if keep_kekule and t:
|
|
83
|
+
if a or any(hgs[n] != aa.implicit_hydrogens for n, aa in self.atoms()):
|
|
84
|
+
s.append(((), -1, 'kekulized again'))
|
|
85
|
+
else:
|
|
86
|
+
s.append(((), -1, 'kekule form restored'))
|
|
87
|
+
return s
|
|
88
|
+
return bool(k or s or h or t or a)
|
|
89
|
+
|
|
90
|
+
def standardize(self, *, logging=False, ignore=True, fix_tautomers=True) -> bool | list:
|
|
91
|
+
"""
|
|
92
|
+
Standardize functional groups. Return True if any non-canonical group found.
|
|
93
|
+
|
|
94
|
+
:param fix_tautomers: convert tautomers to canonical forms.
|
|
95
|
+
:param logging: return list of fixed atoms with matched rules.
|
|
96
|
+
:param ignore: ignore standardization bugs.
|
|
97
|
+
"""
|
|
98
|
+
log, fixed = [], set()
|
|
99
|
+
|
|
100
|
+
l, f = self.__standardize(fragment_rules, fix_tautomers)
|
|
101
|
+
log.extend(l)
|
|
102
|
+
fixed.update(f)
|
|
103
|
+
l, f = self.__standardize(metal_rules, fix_tautomers) # metal-organics fix
|
|
104
|
+
log.extend(l)
|
|
105
|
+
fixed.update(f)
|
|
106
|
+
|
|
107
|
+
if b := fixed.intersection(n for n, a in self.atoms() if a.implicit_hydrogens is None):
|
|
108
|
+
if ignore:
|
|
109
|
+
log.append((tuple(b), -1, 'standardization failed'))
|
|
110
|
+
else:
|
|
111
|
+
raise ImplementationError(f'standardization leads to invalid valences: {b}')
|
|
112
|
+
|
|
113
|
+
if fixed:
|
|
114
|
+
self.fix_stereo()
|
|
115
|
+
|
|
116
|
+
if logging:
|
|
117
|
+
if fixed:
|
|
118
|
+
log.append((tuple(fixed), -1, 'standardized atoms'))
|
|
119
|
+
return log
|
|
120
|
+
return bool(fixed)
|
|
121
|
+
|
|
122
|
+
def standardize_isomers(self, *, logging=False) -> bool | list:
|
|
123
|
+
"""
|
|
124
|
+
Set canonical positions of charges and hydrogens.
|
|
125
|
+
|
|
126
|
+
:param logging: return list of changed atoms.
|
|
127
|
+
"""
|
|
128
|
+
changed: list[int] = []
|
|
129
|
+
bonds = self._bonds
|
|
130
|
+
atoms = self._atoms
|
|
131
|
+
|
|
132
|
+
seen = set()
|
|
133
|
+
charge_morgan_pairs = []
|
|
134
|
+
fcr = []
|
|
135
|
+
tautomer_morgan_pairs = []
|
|
136
|
+
nitrogens = set()
|
|
137
|
+
doubles = set()
|
|
138
|
+
protonated = set()
|
|
139
|
+
carbons = set()
|
|
140
|
+
|
|
141
|
+
for q, rule_type in isomer_rules:
|
|
142
|
+
for mapping in q.get_mapping(self, automorphism_filter=False):
|
|
143
|
+
match = set(mapping.values())
|
|
144
|
+
if len(match.intersection(seen)) > 2:
|
|
145
|
+
continue
|
|
146
|
+
seen.update(match)
|
|
147
|
+
|
|
148
|
+
if rule_type == 'charge_fixed':
|
|
149
|
+
atom_1, atom_2 = mapping[1], mapping[2]
|
|
150
|
+
atoms[atom_1]._charge = 0
|
|
151
|
+
atoms[atom_2]._charge = 1
|
|
152
|
+
changed.append(atom_1)
|
|
153
|
+
changed.append(atom_2)
|
|
154
|
+
|
|
155
|
+
elif rule_type == 'charge_fixed_bridge':
|
|
156
|
+
atom_1, atom_2, atom_3 = mapping[1], mapping[2], mapping[3]
|
|
157
|
+
atoms[atom_3]._charge = 0
|
|
158
|
+
atoms[atom_2]._charge = 1
|
|
159
|
+
changed.append(atom_3)
|
|
160
|
+
changed.append(atom_2)
|
|
161
|
+
|
|
162
|
+
elif rule_type == 'tautomer_fixed':
|
|
163
|
+
atom_1, atom_2 = mapping[1], mapping[2]
|
|
164
|
+
atoms[atom_1]._implicit_hydrogens = 1
|
|
165
|
+
atoms[atom_2]._implicit_hydrogens = 0
|
|
166
|
+
changed.append(atom_1)
|
|
167
|
+
changed.append(atom_2)
|
|
168
|
+
|
|
169
|
+
elif rule_type == 'charge_morgan':
|
|
170
|
+
atom_1, atom_2 = mapping[1], mapping[2]
|
|
171
|
+
atoms[atom_1]._charge = 0
|
|
172
|
+
charge_morgan_pairs.append((atom_1, atom_2, atom_1))
|
|
173
|
+
|
|
174
|
+
elif rule_type == 'charge_morgan_bridge':
|
|
175
|
+
atom_1, atom_2, atom_3 = mapping[1], mapping[2], mapping[3]
|
|
176
|
+
atoms[atom_3]._charge = 0
|
|
177
|
+
charge_morgan_pairs.append((atom_1, atom_2, atom_3))
|
|
178
|
+
|
|
179
|
+
elif rule_type == 'ferrocene':
|
|
180
|
+
atoms[mapping[1]]._charge = 0
|
|
181
|
+
fcr.append([mapping[i] for i in range(1, 6)])
|
|
182
|
+
changed.append(mapping[1])
|
|
183
|
+
|
|
184
|
+
elif rule_type == 'tautomer_morgan':
|
|
185
|
+
atom_1, atom_2 = mapping[1], mapping[2]
|
|
186
|
+
atoms[atom_1]._implicit_hydrogens = 0
|
|
187
|
+
tautomer_morgan_pairs.append((atom_1, atom_2, atom_1))
|
|
188
|
+
|
|
189
|
+
elif rule_type == 'tautomer_morgan_donor':
|
|
190
|
+
atom_1, atom_2, atom_3 = mapping[1], mapping[2], mapping[3]
|
|
191
|
+
atoms[atom_3]._implicit_hydrogens = 0
|
|
192
|
+
tautomer_morgan_pairs.append((atom_1, atom_2, atom_3))
|
|
193
|
+
|
|
194
|
+
elif rule_type == 'amidine':
|
|
195
|
+
atom_1, atom_2 = mapping[1], mapping[2]
|
|
196
|
+
nitrogens.add(atom_2)
|
|
197
|
+
if atom_1 not in nitrogens:
|
|
198
|
+
atom_3 = mapping[3]
|
|
199
|
+
carbons.add(atom_3)
|
|
200
|
+
nitrogens.add(atom_1)
|
|
201
|
+
bonds[atom_1][atom_3]._order = 1
|
|
202
|
+
atoms[atom_1]._implicit_hydrogens += 1
|
|
203
|
+
doubles.add((atom_1, atom_3))
|
|
204
|
+
protonated.add(atom_1)
|
|
205
|
+
|
|
206
|
+
if charge_morgan_pairs or fcr or tautomer_morgan_pairs or carbons:
|
|
207
|
+
self.__dict__.pop('atoms_order', None)
|
|
208
|
+
self.__dict__.pop('int_adjacency', None)
|
|
209
|
+
|
|
210
|
+
for atom_1, atom_2, source in charge_morgan_pairs:
|
|
211
|
+
if self.atoms_order[atom_1] > self.atoms_order[atom_2]:
|
|
212
|
+
atoms[atom_2]._charge = 1
|
|
213
|
+
if source != atom_2:
|
|
214
|
+
changed.append(source)
|
|
215
|
+
changed.append(atom_2)
|
|
216
|
+
else:
|
|
217
|
+
atoms[atom_1]._charge = 1
|
|
218
|
+
if source != atom_1:
|
|
219
|
+
changed.append(source)
|
|
220
|
+
changed.append(atom_1)
|
|
221
|
+
|
|
222
|
+
for ca in fcr:
|
|
223
|
+
n = min(ca, key=self.atoms_order.get)
|
|
224
|
+
atoms[n]._charge = -1
|
|
225
|
+
changed.append(n)
|
|
226
|
+
|
|
227
|
+
for atom_1, atom_2, source in tautomer_morgan_pairs:
|
|
228
|
+
if self.atoms_order[atom_1] >= self.atoms_order[atom_2]:
|
|
229
|
+
atoms[atom_1]._implicit_hydrogens = 1
|
|
230
|
+
if source != atom_1:
|
|
231
|
+
changed.append(source)
|
|
232
|
+
changed.append(atom_1)
|
|
233
|
+
else:
|
|
234
|
+
atoms[atom_2]._implicit_hydrogens = 1
|
|
235
|
+
if source != atom_2:
|
|
236
|
+
changed.append(source)
|
|
237
|
+
changed.append(atom_2)
|
|
238
|
+
|
|
239
|
+
if carbons:
|
|
240
|
+
nitrogens_sorted = sorted(nitrogens, key=self.atoms_order.get)
|
|
241
|
+
while nitrogens_sorted:
|
|
242
|
+
atom_1 = nitrogens_sorted.pop()
|
|
243
|
+
for atom_3 in sorted(bonds[atom_1], key=self.atoms_order.get):
|
|
244
|
+
if atom_3 in carbons:
|
|
245
|
+
carbons.discard(atom_3)
|
|
246
|
+
atoms[atom_1]._implicit_hydrogens -= 1
|
|
247
|
+
bonds[atom_1][atom_3]._order = 2
|
|
248
|
+
if (atom_1, atom_3) not in doubles:
|
|
249
|
+
changed.append(atom_1)
|
|
250
|
+
changed.append(atom_3)
|
|
251
|
+
break
|
|
252
|
+
else:
|
|
253
|
+
if atom_1 in protonated:
|
|
254
|
+
changed.append(atom_1)
|
|
255
|
+
|
|
256
|
+
del self.__dict__['atoms_order']
|
|
257
|
+
del self.__dict__['int_adjacency']
|
|
258
|
+
|
|
259
|
+
if changed:
|
|
260
|
+
self.flush_cache(keep_sssr=True, keep_components=True, keep_special_connectivity=True)
|
|
261
|
+
self.fix_stereo()
|
|
262
|
+
if logging:
|
|
263
|
+
return changed
|
|
264
|
+
return True
|
|
265
|
+
if logging:
|
|
266
|
+
return []
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
def remove_coordinate_bonds(self, *, keep_to_terminal=True) -> int:
|
|
270
|
+
"""Remove coordinate (or hydrogen) bonds marked with 8 (any) bond
|
|
271
|
+
|
|
272
|
+
:param keep_to_terminal: Keep any bonds to terminal hydrogens
|
|
273
|
+
:return: removed bonds count
|
|
274
|
+
"""
|
|
275
|
+
ab = [(n, m) for n, m, b in self.bonds() if b == 8]
|
|
276
|
+
|
|
277
|
+
if keep_to_terminal:
|
|
278
|
+
skeleton = self.not_special_connectivity
|
|
279
|
+
hs = {n for n, a in self.atoms() if a == H and not skeleton[n]}
|
|
280
|
+
ab = [(n, m) for n, m in ab if n not in hs and m not in hs]
|
|
281
|
+
|
|
282
|
+
for n, m in ab:
|
|
283
|
+
self.delete_bond(n, m, _skip_calculation=True)
|
|
284
|
+
|
|
285
|
+
if ab:
|
|
286
|
+
self.flush_cache(keep_sssr=True, keep_special_connectivity=True)
|
|
287
|
+
self.calc_labels()
|
|
288
|
+
self.fix_stereo()
|
|
289
|
+
return len(ab)
|
|
290
|
+
|
|
291
|
+
def implicify_hydrogens(self, *, logging=False) -> int | tuple:
|
|
292
|
+
"""
|
|
293
|
+
Remove explicit hydrogen if possible. Return number of removed hydrogens.
|
|
294
|
+
Works only with Kekule forms of aromatic structures.
|
|
295
|
+
Keeps isotopes of hydrogen.
|
|
296
|
+
|
|
297
|
+
:param logging: return list of changed atoms.
|
|
298
|
+
"""
|
|
299
|
+
atoms = self._atoms
|
|
300
|
+
bonds = self._bonds
|
|
301
|
+
|
|
302
|
+
explicit = defaultdict(list)
|
|
303
|
+
for n, atom in atoms.items():
|
|
304
|
+
if atom == H and (atom.isotope is None or atom.isotope == 1):
|
|
305
|
+
if len(bonds[n]) > 1:
|
|
306
|
+
raise ValenceError(f'Hydrogen atom {n} has invalid valence. Try to use remove_coordinate_bonds()')
|
|
307
|
+
for m, b in bonds[n].items():
|
|
308
|
+
if b == 1:
|
|
309
|
+
if atoms[m] != H: # not H-H
|
|
310
|
+
explicit[m].append(n)
|
|
311
|
+
elif b != 8:
|
|
312
|
+
raise ValenceError(f'Hydrogen atom {n} has invalid valence {b.order}.')
|
|
313
|
+
|
|
314
|
+
to_remove = set()
|
|
315
|
+
fixed = {}
|
|
316
|
+
for n, hs in explicit.items():
|
|
317
|
+
atom = atoms[n]
|
|
318
|
+
len_h = len(hs)
|
|
319
|
+
for i in range(len_h, 0, -1):
|
|
320
|
+
hi = hs[:i]
|
|
321
|
+
explicit_sum = 0
|
|
322
|
+
explicit_dict = defaultdict(int)
|
|
323
|
+
for m, bond in bonds[n].items():
|
|
324
|
+
if m not in hi and bond != 8:
|
|
325
|
+
explicit_sum += bond.order
|
|
326
|
+
explicit_dict[(bond.order, atoms[m].atomic_number)] += 1
|
|
327
|
+
try:
|
|
328
|
+
# aromatic rings don't match any rule
|
|
329
|
+
rules = atom.valence_rules(explicit_sum)
|
|
330
|
+
except ValenceError:
|
|
331
|
+
break
|
|
332
|
+
for s, d, h in rules:
|
|
333
|
+
if s.issubset(explicit_dict) and all(explicit_dict[k] >= c for k, c in d.items()) and h >= i:
|
|
334
|
+
to_remove.update(hi)
|
|
335
|
+
fixed[n] = h
|
|
336
|
+
break
|
|
337
|
+
else:
|
|
338
|
+
continue
|
|
339
|
+
break
|
|
340
|
+
|
|
341
|
+
for n in to_remove:
|
|
342
|
+
del atoms[n]
|
|
343
|
+
for m in bonds.pop(n):
|
|
344
|
+
del bonds[m][n]
|
|
345
|
+
|
|
346
|
+
for n, h in fixed.items():
|
|
347
|
+
atoms[n]._implicit_hydrogens = h
|
|
348
|
+
|
|
349
|
+
if to_remove:
|
|
350
|
+
self.flush_cache(keep_sssr=True)
|
|
351
|
+
self.calc_labels()
|
|
352
|
+
self.fix_stereo()
|
|
353
|
+
|
|
354
|
+
if logging:
|
|
355
|
+
return len(to_remove), list(fixed)
|
|
356
|
+
return len(to_remove)
|
|
357
|
+
|
|
358
|
+
def explicify_hydrogens(self, *, start_map=None, _return_map=False) -> int | list:
|
|
359
|
+
"""
|
|
360
|
+
Add explicit hydrogens to atoms.
|
|
361
|
+
|
|
362
|
+
:return: number of added atoms
|
|
363
|
+
"""
|
|
364
|
+
atoms = self._atoms
|
|
365
|
+
to_add = []
|
|
366
|
+
for n, a in atoms.items():
|
|
367
|
+
try:
|
|
368
|
+
to_add.extend([n] * a.implicit_hydrogens)
|
|
369
|
+
except TypeError:
|
|
370
|
+
raise ValenceError(f'atom {n} has valence error')
|
|
371
|
+
|
|
372
|
+
if to_add:
|
|
373
|
+
log = []
|
|
374
|
+
bonds = self._bonds
|
|
375
|
+
m = start_map if start_map is not None else max(atoms) + 1
|
|
376
|
+
for n in to_add:
|
|
377
|
+
atoms[m] = _H(implicit_hydrogens=0)
|
|
378
|
+
bonds[n][m] = b = Bond(1)
|
|
379
|
+
bonds[m] = {n: b}
|
|
380
|
+
atoms[n]._implicit_hydrogens = 0
|
|
381
|
+
log.append((n, m))
|
|
382
|
+
m += 1
|
|
383
|
+
|
|
384
|
+
self.flush_cache(keep_sssr=True)
|
|
385
|
+
self.calc_labels()
|
|
386
|
+
self.fix_stereo()
|
|
387
|
+
if _return_map:
|
|
388
|
+
return log
|
|
389
|
+
return len(to_add)
|
|
390
|
+
elif _return_map:
|
|
391
|
+
return []
|
|
392
|
+
return 0
|
|
393
|
+
|
|
394
|
+
def check_valence(self) -> list[int]:
|
|
395
|
+
"""
|
|
396
|
+
Check valences of all atoms.
|
|
397
|
+
|
|
398
|
+
:return: list of invalid atoms
|
|
399
|
+
"""
|
|
400
|
+
# only invalid atoms have None hydrogens.
|
|
401
|
+
return [n for n, a in self.atoms() if a.implicit_hydrogens is None]
|
|
402
|
+
|
|
403
|
+
def clean_isotopes(self) -> bool:
|
|
404
|
+
"""
|
|
405
|
+
Clean isotope marks from molecule.
|
|
406
|
+
Return True if any isotope found.
|
|
407
|
+
"""
|
|
408
|
+
isotopes = [a for _, a in self.atoms() if a.isotope]
|
|
409
|
+
if isotopes:
|
|
410
|
+
for i in isotopes:
|
|
411
|
+
i._isotope = None
|
|
412
|
+
self.flush_cache(keep_sssr=True, keep_components=True, keep_special_connectivity=True)
|
|
413
|
+
self.fix_stereo()
|
|
414
|
+
return True
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
def __standardize(self, rules, fix_tautomers):
|
|
418
|
+
atoms = self._atoms
|
|
419
|
+
bonds = self._bonds
|
|
420
|
+
|
|
421
|
+
log = []
|
|
422
|
+
fixed = set()
|
|
423
|
+
for r, (pattern, atom_fix, bonds_fix, any_atoms, is_tautomer) in enumerate(rules):
|
|
424
|
+
if not fix_tautomers and is_tautomer:
|
|
425
|
+
continue
|
|
426
|
+
keep_sssr = keep_components = True
|
|
427
|
+
hs = set()
|
|
428
|
+
seen = set()
|
|
429
|
+
for mapping in pattern.get_mapping(self, automorphism_filter=False):
|
|
430
|
+
match = set(mapping.values())
|
|
431
|
+
if not match.isdisjoint(seen): # skip intersected groups
|
|
432
|
+
continue
|
|
433
|
+
if any_atoms: # accept overlapping of Any-atoms
|
|
434
|
+
seen.update(match - {mapping[n] for n in any_atoms})
|
|
435
|
+
else:
|
|
436
|
+
seen.update(match)
|
|
437
|
+
for n, (ch, ir) in atom_fix.items():
|
|
438
|
+
n = mapping[n]
|
|
439
|
+
hs.add(n)
|
|
440
|
+
a = atoms[n]
|
|
441
|
+
a._charge += ch
|
|
442
|
+
if a.charge > 4:
|
|
443
|
+
a._charge -= ch
|
|
444
|
+
log.append((tuple(match), r, f'bad charge formed. changes omitted: {pattern}'))
|
|
445
|
+
break # skip changes
|
|
446
|
+
if ir is not None:
|
|
447
|
+
a._is_radical = ir
|
|
448
|
+
else:
|
|
449
|
+
for n, m, bo in bonds_fix:
|
|
450
|
+
n = mapping[n]
|
|
451
|
+
m = mapping[m]
|
|
452
|
+
hs.add(n)
|
|
453
|
+
hs.add(m)
|
|
454
|
+
if m in bonds[n]:
|
|
455
|
+
b = bonds[n][m]
|
|
456
|
+
if b == 8 or bo == 8:
|
|
457
|
+
keep_sssr = False
|
|
458
|
+
b._order = bo
|
|
459
|
+
else: # new bond
|
|
460
|
+
keep_sssr = keep_components = False
|
|
461
|
+
bonds[n][m] = bonds[m][n] = Bond(bo)
|
|
462
|
+
log.append((tuple(match), r, str(pattern)))
|
|
463
|
+
|
|
464
|
+
if not hs: # not matched
|
|
465
|
+
continue
|
|
466
|
+
self.flush_cache(keep_sssr=keep_sssr, keep_components=keep_components, keep_special_connectivity=keep_sssr)
|
|
467
|
+
# recalculate isomorphism labels
|
|
468
|
+
self.calc_labels()
|
|
469
|
+
for n in hs: # hydrogens count recalculation
|
|
470
|
+
self.calc_implicit(n)
|
|
471
|
+
fixed.update(hs)
|
|
472
|
+
return log, fixed
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
__all__ = ['Standardize']
|