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,530 @@
|
|
|
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 collections import defaultdict, deque
|
|
20
|
+
from typing import List, Optional, Tuple, TYPE_CHECKING, Union
|
|
21
|
+
from ._rules import rules
|
|
22
|
+
from ..._functions import lazy_product
|
|
23
|
+
from ...exceptions import InvalidAromaticRing
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from chython import MoleculeContainer
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# atomic number constants
|
|
31
|
+
B = 5
|
|
32
|
+
C = 6
|
|
33
|
+
N = 7
|
|
34
|
+
O = 8
|
|
35
|
+
P = 15
|
|
36
|
+
S = 16
|
|
37
|
+
As = 33
|
|
38
|
+
Se = 34
|
|
39
|
+
Te = 52
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Kekule:
|
|
43
|
+
__slots__ = ()
|
|
44
|
+
|
|
45
|
+
def kekule(self: Union['Kekule', 'MoleculeContainer'], *, buffer_size=7) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Convert structure to kekule form. Return True if found any aromatic ring. Set implicit hydrogen count and
|
|
48
|
+
hybridization marks on atoms.
|
|
49
|
+
|
|
50
|
+
Only one of possible double/single bonds positions will be set.
|
|
51
|
+
For enumerate bonds positions use `enumerate_kekule`.
|
|
52
|
+
|
|
53
|
+
:param buffer_size: number of attempts of pyridine form searching.
|
|
54
|
+
"""
|
|
55
|
+
fixed = self.__fix_rings() # fix bad aromatic rings
|
|
56
|
+
kekule = next(self.__kekule_full(buffer_size), None)
|
|
57
|
+
if kekule:
|
|
58
|
+
bonds = self._bonds
|
|
59
|
+
atoms = set()
|
|
60
|
+
for n, m, b in kekule:
|
|
61
|
+
bonds[n][m]._order = b
|
|
62
|
+
atoms.add(n)
|
|
63
|
+
atoms.add(m)
|
|
64
|
+
for n in atoms:
|
|
65
|
+
self.calc_implicit(n)
|
|
66
|
+
self.flush_cache(keep_sssr=True, keep_components=True)
|
|
67
|
+
self.calc_labels()
|
|
68
|
+
return True
|
|
69
|
+
return fixed
|
|
70
|
+
|
|
71
|
+
def enumerate_kekule(self: Union['Kekule', 'MoleculeContainer']):
|
|
72
|
+
"""
|
|
73
|
+
Enumerate all possible kekule forms of molecule.
|
|
74
|
+
"""
|
|
75
|
+
self.__fix_rings() # fix bad aromatic rings
|
|
76
|
+
for form in self.__kekule_full(0):
|
|
77
|
+
copy = self.copy(keep_sssr=True, keep_components=True)
|
|
78
|
+
bonds = copy._bonds
|
|
79
|
+
atoms = set()
|
|
80
|
+
for n, m, b in form:
|
|
81
|
+
bonds[n][m]._order = b
|
|
82
|
+
atoms.add(n)
|
|
83
|
+
atoms.add(m)
|
|
84
|
+
for n in atoms:
|
|
85
|
+
copy.calc_implicit(n)
|
|
86
|
+
copy.calc_labels()
|
|
87
|
+
yield copy
|
|
88
|
+
|
|
89
|
+
def __fix_rings(self: 'MoleculeContainer'):
|
|
90
|
+
atoms = self._atoms
|
|
91
|
+
bonds = self._bonds
|
|
92
|
+
seen = set()
|
|
93
|
+
keep = True
|
|
94
|
+
for q, af, bf, mm in rules:
|
|
95
|
+
for mapping in q.get_mapping(self, automorphism_filter=False):
|
|
96
|
+
match = set(mapping.values())
|
|
97
|
+
if not mm and not match.isdisjoint(seen): # prevent double patching of atoms
|
|
98
|
+
continue
|
|
99
|
+
seen.update(match)
|
|
100
|
+
|
|
101
|
+
for n, c in af.items():
|
|
102
|
+
n = mapping[n]
|
|
103
|
+
atoms[n]._charge = c
|
|
104
|
+
for n, m, b in bf:
|
|
105
|
+
n = mapping[n]
|
|
106
|
+
m = mapping[m]
|
|
107
|
+
bonds[n][m]._order = b
|
|
108
|
+
if b == 8:
|
|
109
|
+
# flush sssr and components cache
|
|
110
|
+
keep = False
|
|
111
|
+
if seen:
|
|
112
|
+
self.flush_cache(keep_sssr=keep, keep_components=keep)
|
|
113
|
+
self.calc_labels()
|
|
114
|
+
return True
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def __prepare_rings(self: 'MoleculeContainer'):
|
|
118
|
+
atoms = self._atoms
|
|
119
|
+
bonds = self._bonds
|
|
120
|
+
|
|
121
|
+
rings = defaultdict(list) # aromatic skeleton
|
|
122
|
+
pyrroles = set()
|
|
123
|
+
|
|
124
|
+
double_bonded = defaultdict(list)
|
|
125
|
+
triple_bonded = set()
|
|
126
|
+
for n, m_bond in bonds.items():
|
|
127
|
+
for m, bond in m_bond.items():
|
|
128
|
+
if bond == 4:
|
|
129
|
+
rings[n].append(m)
|
|
130
|
+
elif bond == 2:
|
|
131
|
+
double_bonded[n].append(m)
|
|
132
|
+
elif bond == 3:
|
|
133
|
+
triple_bonded.add(n)
|
|
134
|
+
|
|
135
|
+
if not rings:
|
|
136
|
+
return rings, pyrroles, set()
|
|
137
|
+
elif not triple_bonded.isdisjoint(rings):
|
|
138
|
+
raise InvalidAromaticRing('triple bonds connected to rings')
|
|
139
|
+
|
|
140
|
+
copy_rings = {n: ms.copy() for n, ms in rings.items()}
|
|
141
|
+
for r in self.sssr:
|
|
142
|
+
if set(r).issubset(rings):
|
|
143
|
+
n, *_, m = r
|
|
144
|
+
if n not in rings[m]: # fix invalid structures: c1ccc-cc1
|
|
145
|
+
# remove inner ring double bonds: c1ccc=cc1
|
|
146
|
+
if n in double_bonded and m in double_bonded and m in double_bonded[n]:
|
|
147
|
+
double_bonded[n].remove(m)
|
|
148
|
+
double_bonded[m].remove(n)
|
|
149
|
+
rings[m].append(n)
|
|
150
|
+
rings[n].append(m)
|
|
151
|
+
elif m in copy_rings[n]:
|
|
152
|
+
copy_rings[n].remove(m)
|
|
153
|
+
copy_rings[m].remove(n)
|
|
154
|
+
for n, m in zip(r, r[1:]):
|
|
155
|
+
if n not in rings[m]:
|
|
156
|
+
if n in double_bonded and m in double_bonded and m in double_bonded[n]:
|
|
157
|
+
double_bonded[n].remove(m)
|
|
158
|
+
double_bonded[m].remove(n)
|
|
159
|
+
rings[m].append(n)
|
|
160
|
+
rings[n].append(m)
|
|
161
|
+
elif m in copy_rings[n]:
|
|
162
|
+
copy_rings[n].remove(m)
|
|
163
|
+
copy_rings[m].remove(n)
|
|
164
|
+
|
|
165
|
+
# fix invalid smiles: c1ccccc1c2ccccc2 instead of c1ccccc1-c2ccccc2
|
|
166
|
+
seen = set()
|
|
167
|
+
for n, ms in copy_rings.items():
|
|
168
|
+
if ms:
|
|
169
|
+
seen.add(n)
|
|
170
|
+
for m in ms:
|
|
171
|
+
if m not in seen:
|
|
172
|
+
rings[n].remove(m)
|
|
173
|
+
rings[m].remove(n)
|
|
174
|
+
bonds[n][m]._order = 1
|
|
175
|
+
|
|
176
|
+
if any(len(ms) not in (2, 3) for ms in rings.values()):
|
|
177
|
+
raise InvalidAromaticRing('not in ring aromatic bond or hypercondensed rings: '
|
|
178
|
+
f'{{{", ".join(str(n) for n, ms in rings.items() if len(ms) not in (2, 3))}}}')
|
|
179
|
+
|
|
180
|
+
# get double bonded ring atoms
|
|
181
|
+
double_bonded = {n for n, ms in double_bonded.items() if ms and n in rings}
|
|
182
|
+
if any(len(rings[n]) != 2 for n in double_bonded): # double bonded never condensed
|
|
183
|
+
raise InvalidAromaticRing('quinone valence error')
|
|
184
|
+
for n in double_bonded:
|
|
185
|
+
if (atom := atoms[n]) == N:
|
|
186
|
+
if atom.charge != 1:
|
|
187
|
+
raise InvalidAromaticRing('quinone should be charged N atom')
|
|
188
|
+
elif atom not in (C, P, S, As, Se, Te) or atom.charge:
|
|
189
|
+
raise InvalidAromaticRing('quinone should be neutral S, Se, Te, C, P, As atom')
|
|
190
|
+
|
|
191
|
+
for n in rings:
|
|
192
|
+
if (atom := atoms[n]) == C: # carbon
|
|
193
|
+
if atom.charge == 0:
|
|
194
|
+
if atom.neighbors not in (2, 3):
|
|
195
|
+
raise InvalidAromaticRing
|
|
196
|
+
elif atom.charge in (-1, 1):
|
|
197
|
+
if atom.is_radical:
|
|
198
|
+
if atom.neighbors == 2:
|
|
199
|
+
double_bonded.add(n)
|
|
200
|
+
else:
|
|
201
|
+
raise InvalidAromaticRing
|
|
202
|
+
elif atom.neighbors == 3:
|
|
203
|
+
double_bonded.add(n)
|
|
204
|
+
elif atom.neighbors == 2: # benzene (an|cat)ion or pyrrole
|
|
205
|
+
pyrroles.add(n)
|
|
206
|
+
else:
|
|
207
|
+
raise InvalidAromaticRing
|
|
208
|
+
else:
|
|
209
|
+
raise InvalidAromaticRing
|
|
210
|
+
elif atom in (N, P, As):
|
|
211
|
+
if atom.charge == 0: # pyrrole or pyridine. include radical pyrrole
|
|
212
|
+
if atom.is_radical:
|
|
213
|
+
if atom.neighbors != 2: # only pyrrole radical
|
|
214
|
+
raise InvalidAromaticRing
|
|
215
|
+
double_bonded.add(n)
|
|
216
|
+
elif atom.neighbors == 3:
|
|
217
|
+
if atom == N: # pyrrole only possible
|
|
218
|
+
double_bonded.add(n)
|
|
219
|
+
else: # P(III) or P(V)H
|
|
220
|
+
pyrroles.add(n)
|
|
221
|
+
elif atom.neighbors == 2:
|
|
222
|
+
if atom.implicit_hydrogens is None: # pyrrole or pyridine
|
|
223
|
+
pyrroles.add(n)
|
|
224
|
+
elif atom.implicit_hydrogens == 1: # only pyrrole
|
|
225
|
+
double_bonded.add(n)
|
|
226
|
+
elif atom.implicit_hydrogens: # too many hydrogens for aromatic rings
|
|
227
|
+
raise InvalidAromaticRing
|
|
228
|
+
elif atom.neighbors != 4 or atom not in (P, As): # P(V) in ring [P;a](-R1)-R2
|
|
229
|
+
raise InvalidAromaticRing
|
|
230
|
+
elif atom.charge == -1: # pyrrole only
|
|
231
|
+
if atom.neighbors != 2 or atom.is_radical:
|
|
232
|
+
raise InvalidAromaticRing
|
|
233
|
+
double_bonded.add(n)
|
|
234
|
+
elif atom.charge != 1:
|
|
235
|
+
raise InvalidAromaticRing
|
|
236
|
+
elif atom.is_radical:
|
|
237
|
+
if atom.neighbors != 2: # not cation-radical pyridine
|
|
238
|
+
raise InvalidAromaticRing
|
|
239
|
+
elif atom.neighbors == 2: # pyrrole cation or protonated pyridine
|
|
240
|
+
pyrroles.add(n)
|
|
241
|
+
elif atom.neighbors != 3: # not pyridine oxyde
|
|
242
|
+
raise InvalidAromaticRing
|
|
243
|
+
elif atom == O: # furan
|
|
244
|
+
if atom.neighbors == 2:
|
|
245
|
+
if atom.charge == 0:
|
|
246
|
+
if atom.is_radical:
|
|
247
|
+
raise InvalidAromaticRing('radical oxygen')
|
|
248
|
+
double_bonded.add(n)
|
|
249
|
+
elif atom.charge == 1:
|
|
250
|
+
if atom.is_radical: # furan cation-radical
|
|
251
|
+
double_bonded.add(n)
|
|
252
|
+
# pyrylium
|
|
253
|
+
else:
|
|
254
|
+
raise InvalidAromaticRing('invalid oxygen charge')
|
|
255
|
+
else:
|
|
256
|
+
raise InvalidAromaticRing('Triple-bonded oxygen')
|
|
257
|
+
elif atom in (S, Se, Te): # thiophene
|
|
258
|
+
if n not in double_bonded: # not sulphoxyde nor sulphone
|
|
259
|
+
if atom.neighbors == 2:
|
|
260
|
+
if atom.is_radical:
|
|
261
|
+
if atom.charge == 1:
|
|
262
|
+
double_bonded.add(n)
|
|
263
|
+
else:
|
|
264
|
+
raise InvalidAromaticRing('S, Se, Te cation-radical expected')
|
|
265
|
+
if atom.charge == 0:
|
|
266
|
+
double_bonded.add(n)
|
|
267
|
+
elif atom.charge != 1:
|
|
268
|
+
raise InvalidAromaticRing('S, Se, Te cation in benzene like ring expected')
|
|
269
|
+
elif atom.neighbors == 3:
|
|
270
|
+
if atom.is_radical:
|
|
271
|
+
if atom.charge:
|
|
272
|
+
raise InvalidAromaticRing('S, Se, Te ion-radical ring')
|
|
273
|
+
double_bonded.add(n)
|
|
274
|
+
elif atom.charge == 1:
|
|
275
|
+
double_bonded.add(n)
|
|
276
|
+
elif atom.charge:
|
|
277
|
+
raise InvalidAromaticRing('S, Se, Te invalid charge ring')
|
|
278
|
+
else:
|
|
279
|
+
raise InvalidAromaticRing('S, Se, Te hypervalent ring')
|
|
280
|
+
elif atom == B:
|
|
281
|
+
if atom.charge == 0:
|
|
282
|
+
if atom.neighbors == 2:
|
|
283
|
+
if atom.is_radical: # C=1O[B]OC=1
|
|
284
|
+
double_bonded.add(n)
|
|
285
|
+
else:
|
|
286
|
+
if atom.implicit_hydrogens is None: # b1ccccc1, C=1OBOC=1 or B1C=CC=N1
|
|
287
|
+
pyrroles.add(n)
|
|
288
|
+
elif atom.implicit_hydrogens == 1: # C=1O[BH]OC=1 or [BH]1C=CC=N1
|
|
289
|
+
double_bonded.add(n)
|
|
290
|
+
elif atom.implicit_hydrogens:
|
|
291
|
+
raise InvalidAromaticRing
|
|
292
|
+
elif not atom.is_radical:
|
|
293
|
+
double_bonded.add(n)
|
|
294
|
+
else:
|
|
295
|
+
raise InvalidAromaticRing
|
|
296
|
+
elif atom.charge == 1:
|
|
297
|
+
if atom.neighbors == 2 and not atom.is_radical:
|
|
298
|
+
double_bonded.add(n)
|
|
299
|
+
else:
|
|
300
|
+
raise InvalidAromaticRing
|
|
301
|
+
elif atom.charge == -1:
|
|
302
|
+
if atom.neighbors == 2:
|
|
303
|
+
if not atom.is_radical: # C=1O[B-]OC=1 or [bH-]1ccccc1
|
|
304
|
+
pyrroles.add(n)
|
|
305
|
+
# anion-radical is benzene like
|
|
306
|
+
elif atom.is_radical: # C=1O[B-*](R)OC=1
|
|
307
|
+
double_bonded.add(n)
|
|
308
|
+
else:
|
|
309
|
+
pyrroles.add(n)
|
|
310
|
+
else:
|
|
311
|
+
raise InvalidAromaticRing
|
|
312
|
+
else:
|
|
313
|
+
raise InvalidAromaticRing(f'only B, C, N, P, O, S, Se, Te possible, not: {atoms[n].atomic_symbol}')
|
|
314
|
+
return rings, pyrroles, double_bonded
|
|
315
|
+
|
|
316
|
+
def __kekule_full(self, buffer_size):
|
|
317
|
+
rings, pyrroles, double_bonded = self.__prepare_rings()
|
|
318
|
+
atoms = set(rings)
|
|
319
|
+
components = []
|
|
320
|
+
while atoms:
|
|
321
|
+
start = atoms.pop()
|
|
322
|
+
component = {start: rings[start]}
|
|
323
|
+
queue = deque([start])
|
|
324
|
+
while queue:
|
|
325
|
+
current = queue.popleft()
|
|
326
|
+
for n in rings[current]:
|
|
327
|
+
if n not in component:
|
|
328
|
+
queue.append(n)
|
|
329
|
+
component[n] = rings[n]
|
|
330
|
+
|
|
331
|
+
components.append(component)
|
|
332
|
+
atoms.difference_update(component)
|
|
333
|
+
|
|
334
|
+
for keks in lazy_product(*(_kekule_component(c, double_bonded & c.keys(), pyrroles & c.keys(), buffer_size)
|
|
335
|
+
for c in components)):
|
|
336
|
+
yield [x for x in keks for x in x]
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _kekule_component(rings, double_bonded, pyrroles, buffer_size):
|
|
340
|
+
# (current atom, previous atom, bond between cp atoms, path deep for cutting [None if cut impossible])
|
|
341
|
+
stack: List[List[Tuple[int, int, int, Optional[int]]]]
|
|
342
|
+
if double_bonded: # start from double bonded if exists
|
|
343
|
+
start = next(iter(double_bonded))
|
|
344
|
+
stack = [[(next(iter(rings[start])), start, 1, 0)]]
|
|
345
|
+
else: # select not pyrrole not condensed atom
|
|
346
|
+
try:
|
|
347
|
+
start = next(n for n, ms in rings.items() if len(ms) == 2 and n not in pyrroles)
|
|
348
|
+
except StopIteration: # all pyrroles. select not condensed atom.
|
|
349
|
+
try:
|
|
350
|
+
start = next(n for n, ms in rings.items() if len(ms) == 2)
|
|
351
|
+
except StopIteration: # fullerene?
|
|
352
|
+
start = next(iter(rings))
|
|
353
|
+
double_bonded.add(start)
|
|
354
|
+
stack = [[(next_atom, start, 2, 0)] for next_atom in rings[start]]
|
|
355
|
+
else:
|
|
356
|
+
stack = [[(next_atom, start, 1, 0)] for next_atom in rings[start]]
|
|
357
|
+
else:
|
|
358
|
+
stack = [[(next_atom, start, 1, 0)] for next_atom in rings[start]]
|
|
359
|
+
|
|
360
|
+
size = sum(len(x) for x in rings.values()) // 2
|
|
361
|
+
path = []
|
|
362
|
+
hashed_path = set()
|
|
363
|
+
nether_yielded = True
|
|
364
|
+
buffer = []
|
|
365
|
+
|
|
366
|
+
while stack:
|
|
367
|
+
atom, prev_atom, bond, _ = stack[-1].pop()
|
|
368
|
+
path.append((atom, prev_atom, bond))
|
|
369
|
+
hashed_path.add(atom)
|
|
370
|
+
|
|
371
|
+
if len(path) == size:
|
|
372
|
+
if nether_yielded:
|
|
373
|
+
nether_yielded = False
|
|
374
|
+
if pyrroles and buffer_size: # prioritize pyridine over pyrrole
|
|
375
|
+
g = defaultdict(int)
|
|
376
|
+
for n, m, b in path:
|
|
377
|
+
g[n] += b
|
|
378
|
+
g[m] += b
|
|
379
|
+
# should be pairs of pyrrole atoms
|
|
380
|
+
if sum(b == 2 and n in pyrroles for n, b in g.items()) >= 2:
|
|
381
|
+
if len(buffer) == buffer_size: # optimization. try only few times to prevent freezes.
|
|
382
|
+
buffer_size = 0 # disable bufferization
|
|
383
|
+
yield from buffer
|
|
384
|
+
yield path
|
|
385
|
+
buffer = []
|
|
386
|
+
else:
|
|
387
|
+
buffer.append(path)
|
|
388
|
+
else:
|
|
389
|
+
yield path
|
|
390
|
+
buffer_size = 0 # disable bufferization
|
|
391
|
+
if buffer: # empty buffer
|
|
392
|
+
yield from buffer
|
|
393
|
+
buffer = []
|
|
394
|
+
else:
|
|
395
|
+
yield path
|
|
396
|
+
|
|
397
|
+
del stack[-1]
|
|
398
|
+
if stack:
|
|
399
|
+
path = path[:stack[-1][-1][-1]]
|
|
400
|
+
hashed_path = {x for x, *_ in path}
|
|
401
|
+
elif atom != start:
|
|
402
|
+
for_stack = []
|
|
403
|
+
closures = []
|
|
404
|
+
loop = 0
|
|
405
|
+
for next_atom in rings[atom]:
|
|
406
|
+
if next_atom == prev_atom: # only forward. behind us is the homeland
|
|
407
|
+
continue
|
|
408
|
+
elif next_atom == start:
|
|
409
|
+
loop = next_atom
|
|
410
|
+
elif next_atom in hashed_path: # closure found
|
|
411
|
+
closures.append(next_atom)
|
|
412
|
+
else:
|
|
413
|
+
for_stack.append(next_atom)
|
|
414
|
+
|
|
415
|
+
if loop: # we found starting point.
|
|
416
|
+
if bond == 2: # finish should be single bonded
|
|
417
|
+
if double_bonded: # ok
|
|
418
|
+
stack[-1].insert(0, (loop, atom, 1, None))
|
|
419
|
+
else:
|
|
420
|
+
del stack[-1]
|
|
421
|
+
if stack:
|
|
422
|
+
path = path[:stack[-1][-1][-1]]
|
|
423
|
+
hashed_path = {x for x, *_ in path}
|
|
424
|
+
continue
|
|
425
|
+
elif double_bonded: # we in quinone ring. finish should be single bonded
|
|
426
|
+
# side-path for storing double bond or atom is quinone or pyrrole
|
|
427
|
+
if for_stack or atom in double_bonded or atom in pyrroles:
|
|
428
|
+
stack[-1].insert(0, (loop, atom, 1, None))
|
|
429
|
+
else:
|
|
430
|
+
del stack[-1]
|
|
431
|
+
if stack:
|
|
432
|
+
path = path[:stack[-1][-1][-1]]
|
|
433
|
+
hashed_path = {x for x, *_ in path}
|
|
434
|
+
continue
|
|
435
|
+
else: # finish should be double bonded
|
|
436
|
+
stack[-1].insert(0, (loop, atom, 2, None))
|
|
437
|
+
bond = 2 # grow should be single bonded
|
|
438
|
+
|
|
439
|
+
if bond == 2 or atom in double_bonded: # double in - single out. quinone has two single bonds
|
|
440
|
+
for next_atom in closures:
|
|
441
|
+
path.append((next_atom, atom, 1)) # closures always single-bonded
|
|
442
|
+
stack[-1].remove((atom, next_atom, 1, None)) # remove fork from stack
|
|
443
|
+
for next_atom in for_stack:
|
|
444
|
+
stack[-1].append((next_atom, atom, 1, None))
|
|
445
|
+
elif len(for_stack) == 1: # easy path grow. next bond double or include single for pyrroles
|
|
446
|
+
next_atom = for_stack[0]
|
|
447
|
+
if next_atom in double_bonded: # need double bond, but next atom quinone
|
|
448
|
+
if atom in pyrroles:
|
|
449
|
+
stack[-1].append((next_atom, atom, 1, None))
|
|
450
|
+
else:
|
|
451
|
+
del stack[-1]
|
|
452
|
+
if stack:
|
|
453
|
+
path = path[:stack[-1][-1][-1]]
|
|
454
|
+
hashed_path = {x for x, *_ in path}
|
|
455
|
+
elif atom in pyrroles: # try pyrrole and pyridine
|
|
456
|
+
opposite = stack[-1].copy()
|
|
457
|
+
opposite.append((next_atom, atom, 2, None))
|
|
458
|
+
stack[-1].append((next_atom, atom, 1, len(path)))
|
|
459
|
+
stack.append(opposite)
|
|
460
|
+
else:
|
|
461
|
+
stack[-1].append((next_atom, atom, 2, None))
|
|
462
|
+
if closures:
|
|
463
|
+
next_atom = closures[0]
|
|
464
|
+
path.append((next_atom, atom, 1)) # closures always single-bonded
|
|
465
|
+
stack[-1].remove((atom, next_atom, 1, None)) # remove fork from stack
|
|
466
|
+
elif for_stack: # fork
|
|
467
|
+
next_atom1, next_atom2 = for_stack
|
|
468
|
+
if next_atom1 in double_bonded: # quinone next from fork
|
|
469
|
+
if next_atom2 in double_bonded:
|
|
470
|
+
if atom in pyrroles: # shit like O=C1C=CC2=CC=CC3=C2P1C(=O)C=C3
|
|
471
|
+
stack[-1].append((next_atom1, atom, 1, None))
|
|
472
|
+
stack[-1].append((next_atom2, atom, 1, None))
|
|
473
|
+
else: # bad path
|
|
474
|
+
del stack[-1]
|
|
475
|
+
if stack:
|
|
476
|
+
path = path[:stack[-1][-1][-1]]
|
|
477
|
+
hashed_path = {x for x, *_ in path}
|
|
478
|
+
elif atom in pyrroles: # O=C1C=CC2=CC=CC3=C2P1C=C3 or O=C1C=CC2=CC=CC3=C2P1=CC=C3
|
|
479
|
+
opposite = stack[-1].copy()
|
|
480
|
+
opposite.append((next_atom1, atom, 1, None))
|
|
481
|
+
opposite.append((next_atom2, atom, 2, None))
|
|
482
|
+
stack[-1].append((next_atom1, atom, 1, None))
|
|
483
|
+
stack[-1].append((next_atom2, atom, 1, len(path)))
|
|
484
|
+
stack.append(opposite) # pyridine first
|
|
485
|
+
else: # normal condensed ring
|
|
486
|
+
stack[-1].append((next_atom1, atom, 1, None))
|
|
487
|
+
stack[-1].append((next_atom2, atom, 2, None))
|
|
488
|
+
elif next_atom2 in double_bonded: # quinone next from fork
|
|
489
|
+
if atom in pyrroles:
|
|
490
|
+
opposite = stack[-1].copy()
|
|
491
|
+
opposite.append((next_atom2, atom, 1, None))
|
|
492
|
+
opposite.append((next_atom1, atom, 2, None))
|
|
493
|
+
stack[-1].append((next_atom1, atom, 1, None))
|
|
494
|
+
stack[-1].append((next_atom2, atom, 1, len(path)))
|
|
495
|
+
stack.append(opposite)
|
|
496
|
+
else:
|
|
497
|
+
stack[-1].append((next_atom2, atom, 1, None))
|
|
498
|
+
stack[-1].append((next_atom1, atom, 2, None))
|
|
499
|
+
elif atom in pyrroles: # C1=CC2=CC=CC3=C2P1C=C3 or C1=CP2=CC=CC3=C2C1=CC=C3
|
|
500
|
+
opposite1 = stack[-1].copy()
|
|
501
|
+
opposite1.append((next_atom2, atom, 1, None))
|
|
502
|
+
opposite1.append((next_atom1, atom, 2, len(path)))
|
|
503
|
+
opposite2 = stack[-1].copy()
|
|
504
|
+
opposite2.append((next_atom1, atom, 1, None))
|
|
505
|
+
opposite2.append((next_atom2, atom, 2, None))
|
|
506
|
+
|
|
507
|
+
stack[-1].append((next_atom1, atom, 1, None))
|
|
508
|
+
stack[-1].append((next_atom2, atom, 1, len(path)))
|
|
509
|
+
stack.append(opposite1)
|
|
510
|
+
stack.append(opposite2)
|
|
511
|
+
else: # new path
|
|
512
|
+
opposite = stack[-1].copy()
|
|
513
|
+
stack[-1].append((next_atom1, atom, 1, None))
|
|
514
|
+
stack[-1].append((next_atom2, atom, 2, len(path))) # double bond on top of stack
|
|
515
|
+
opposite.append((next_atom2, atom, 1, None))
|
|
516
|
+
opposite.append((next_atom1, atom, 2, None))
|
|
517
|
+
stack.append(opposite)
|
|
518
|
+
elif closures and atom not in pyrroles: # need double bond, but closure should be single bonded
|
|
519
|
+
del stack[-1]
|
|
520
|
+
if stack:
|
|
521
|
+
path = path[:stack[-1][-1][-1]]
|
|
522
|
+
hashed_path = {x for x, *_ in path}
|
|
523
|
+
|
|
524
|
+
if nether_yielded:
|
|
525
|
+
raise InvalidAromaticRing(f'kekule form not found for: {list(rings)}')
|
|
526
|
+
elif buffer: # optimal solution not found. return available.
|
|
527
|
+
yield from buffer
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
__all__ = ['Kekule']
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 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
|
+
#
|