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,160 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2025 Tagir Akhmetshin <tagirshin@gmail.com>
|
|
5
|
+
# This file is part of chython.
|
|
6
|
+
#
|
|
7
|
+
# chython is free software; you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
9
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
18
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
#
|
|
20
|
+
from chython import smiles
|
|
21
|
+
from chython.exceptions import InvalidAromaticRing
|
|
22
|
+
from pytest import raises
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_kekule_basic():
|
|
26
|
+
# Test basic aromatic ring conversion
|
|
27
|
+
mol = smiles('c1ccccc1') # benzene
|
|
28
|
+
assert mol.kekule() # should return True for aromatic rings
|
|
29
|
+
assert mol == smiles('C1=CC=CC=C1')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_kekule_pyridine():
|
|
33
|
+
# Test pyridine and its derivatives
|
|
34
|
+
mol = smiles('n1ccccc1') # pyridine
|
|
35
|
+
assert mol.kekule()
|
|
36
|
+
assert mol == smiles('N1=CC=CC=C1')
|
|
37
|
+
assert mol.atom(1).implicit_hydrogens == 0
|
|
38
|
+
|
|
39
|
+
# Test protonated pyridine
|
|
40
|
+
mol = smiles('[nH+]1ccccc1')
|
|
41
|
+
assert mol.kekule()
|
|
42
|
+
assert mol == smiles('[NH+]1=CC=CC=C1')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_kekule_pyrrole():
|
|
46
|
+
# Test pyrrole and its derivatives
|
|
47
|
+
mol = smiles('[nH]1cccc1') # pyrrole
|
|
48
|
+
assert mol.kekule()
|
|
49
|
+
assert mol == smiles('N1C=CC=C1')
|
|
50
|
+
assert mol.atom(1).implicit_hydrogens == 1
|
|
51
|
+
|
|
52
|
+
mol = smiles('n1cccc1')
|
|
53
|
+
assert mol.kekule()
|
|
54
|
+
assert mol == smiles('N1C=CC=C1')
|
|
55
|
+
assert mol.atom(1).implicit_hydrogens == 1
|
|
56
|
+
|
|
57
|
+
# Test N-methylpyrrole
|
|
58
|
+
mol = smiles('Cn1cccc1')
|
|
59
|
+
assert mol.kekule()
|
|
60
|
+
assert mol == smiles('CN1C=CC=C1')
|
|
61
|
+
assert mol.atom(2).implicit_hydrogens == 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_kekule_furan_thiophene():
|
|
65
|
+
# Test oxygen and sulfur containing aromatics
|
|
66
|
+
mol = smiles('o1cccc1')
|
|
67
|
+
assert mol.kekule()
|
|
68
|
+
assert mol == smiles('O1C=CC=C1')
|
|
69
|
+
assert mol.atom(1).implicit_hydrogens == 0
|
|
70
|
+
|
|
71
|
+
mol = smiles('s1cccc1')
|
|
72
|
+
assert mol.kekule()
|
|
73
|
+
assert mol == smiles('S1C=CC=C1')
|
|
74
|
+
assert mol.atom(1).implicit_hydrogens == 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_kekule_complex_systems():
|
|
78
|
+
# Test fused ring systems
|
|
79
|
+
mol = smiles('c1ccc2ccccc2c1')
|
|
80
|
+
assert mol.kekule()
|
|
81
|
+
assert mol == smiles('C1=CC2=CC=CC=C2C=C1') or mol == smiles('C1=CC2=C(C=C1)C=CC=C2')
|
|
82
|
+
|
|
83
|
+
# Test indole
|
|
84
|
+
mol = smiles('c1ccc2[nH]ccc2c1')
|
|
85
|
+
assert mol.kekule()
|
|
86
|
+
assert mol == smiles('N1C=CC2=C1C=CC=C2') or mol == smiles('N1C=CC2=CC=CC=C12')
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_kekule_enumeration():
|
|
90
|
+
mol = smiles('Cc1ccccc1C')
|
|
91
|
+
forms = list(mol.enumerate_kekule())
|
|
92
|
+
assert len(forms) == 2 # benzene has 2 Kekule forms
|
|
93
|
+
assert smiles('CC1=C(C)C=CC=C1') in forms
|
|
94
|
+
assert smiles('CC1=CC=CC=C1C') in forms
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_kekule_invalid_structures():
|
|
98
|
+
# Test invalid aromatic structures
|
|
99
|
+
with raises(InvalidAromaticRing):
|
|
100
|
+
mol = smiles('c1cccc1') # 5-membered carbon ring (invalid aromatic)
|
|
101
|
+
mol.kekule()
|
|
102
|
+
|
|
103
|
+
with raises(InvalidAromaticRing):
|
|
104
|
+
mol = smiles('c1ccc2c1c3ccccc3cc2') # acenaphthalene (invalid aromatic form)
|
|
105
|
+
mol.kekule()
|
|
106
|
+
|
|
107
|
+
with raises(InvalidAromaticRing):
|
|
108
|
+
mol = smiles('c1cccc1C(=O)c1cccc1') # cyclopentadiene with carbonyl (invalid aromatic)
|
|
109
|
+
mol.kekule()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_kekule_charged_species():
|
|
113
|
+
# Test charged aromatic species
|
|
114
|
+
mol = smiles('[n+]1ccccc1')
|
|
115
|
+
assert mol.kekule()
|
|
116
|
+
assert mol == smiles('C=1[NH+]=CC=CC=1')
|
|
117
|
+
|
|
118
|
+
mol = smiles('[cH-]1cccc1')
|
|
119
|
+
assert mol.kekule()
|
|
120
|
+
assert mol == smiles('C=1C=C[CH-]C=1')
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_kekule_multiple_rings():
|
|
124
|
+
# Test molecules with multiple aromatic rings
|
|
125
|
+
mol = smiles('c1ccccc1c2ccccc2')
|
|
126
|
+
assert mol.kekule()
|
|
127
|
+
assert mol == smiles('C1=CC=C(C=C1)C1=CC=CC=C1')
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_kekule_heteroatoms():
|
|
131
|
+
# Test various heteroatoms in aromatic rings
|
|
132
|
+
mol = smiles('c1cncn1') # two nitrogens
|
|
133
|
+
assert mol.kekule()
|
|
134
|
+
assert mol == smiles('N1C=CN=C1')
|
|
135
|
+
|
|
136
|
+
mol = smiles('o1cncc1') # oxygen and nitrogen
|
|
137
|
+
assert mol.kekule()
|
|
138
|
+
assert mol == smiles('C1=COC=N1')
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_kekule_buffer_size():
|
|
142
|
+
# Test buffer size parameter for complex heterocycles
|
|
143
|
+
mol = smiles('c1ccc2[nH]ccc2c1') # indole
|
|
144
|
+
assert mol.kekule(buffer_size=1) # small buffer
|
|
145
|
+
|
|
146
|
+
mol = smiles('c1ccc2[nH]ccc2c1') # fresh indole instance
|
|
147
|
+
assert mol.kekule(buffer_size=10) # large buffer
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_kekule_radical_species():
|
|
151
|
+
mol = smiles('[c]1ccccc1')
|
|
152
|
+
assert mol.kekule()
|
|
153
|
+
assert mol == smiles('C=1C=CC=[C]C=1 |^1:4|')
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_kekule_quinones():
|
|
157
|
+
# Test quinone-like structures
|
|
158
|
+
mol = smiles('O=c1ccc(=O)cc1')
|
|
159
|
+
assert mol.kekule()
|
|
160
|
+
assert mol == smiles('C1=CC(C=CC1=O)=O')
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2025 Tagir Akhmetshin <tagirshin@gmail.com>
|
|
5
|
+
# This file is part of chython.
|
|
6
|
+
#
|
|
7
|
+
# chython is free software; you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
9
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
18
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
#
|
|
20
|
+
from chython import smiles
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_basic_thiele():
|
|
24
|
+
# Test basic aromatic systems
|
|
25
|
+
mol = smiles('C1=CC=CC=C1') # benzene in Kekule form
|
|
26
|
+
assert mol.thiele()
|
|
27
|
+
|
|
28
|
+
# Check that all bonds in the ring are aromatic (order 4)
|
|
29
|
+
for n, m, bond in mol.bonds():
|
|
30
|
+
assert bond.order == 4
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_pyrrole_thiele():
|
|
34
|
+
# Test pyrrole-like systems
|
|
35
|
+
mol = smiles('N1C=CC=C1') # pyrrole in Kekule form
|
|
36
|
+
assert mol.thiele()
|
|
37
|
+
|
|
38
|
+
# Check that all bonds in the ring are aromatic
|
|
39
|
+
for n, m, bond in mol.bonds():
|
|
40
|
+
assert bond.order == 4
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_furan_thiele():
|
|
44
|
+
# Test furan-like systems
|
|
45
|
+
mol = smiles('O1C=CC=C1') # furan in Kekule form
|
|
46
|
+
assert mol.thiele()
|
|
47
|
+
|
|
48
|
+
# Check that all bonds in the ring are aromatic
|
|
49
|
+
for n, m, bond in mol.bonds():
|
|
50
|
+
assert bond.order == 4
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_thiophene_thiele():
|
|
54
|
+
# Test thiophene-like systems
|
|
55
|
+
mol = smiles('S1C=CC=C1') # thiophene in Kekule form
|
|
56
|
+
assert mol.thiele()
|
|
57
|
+
|
|
58
|
+
# Check that all bonds in the ring are aromatic
|
|
59
|
+
for n, m, bond in mol.bonds():
|
|
60
|
+
assert bond.order == 4
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_condensed_rings():
|
|
64
|
+
# Test condensed ring systems
|
|
65
|
+
mol = smiles('C1=CC=C2C=CC=CC2=C1') # naphthalene in Kekule form
|
|
66
|
+
assert mol.thiele()
|
|
67
|
+
|
|
68
|
+
# Check that all bonds in both rings are aromatic
|
|
69
|
+
for n, m, bond in mol.bonds():
|
|
70
|
+
assert bond.order == 4
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_tautomer_fix():
|
|
74
|
+
# Test tautomer fixing in condensed rings
|
|
75
|
+
mol = smiles('N1C=CC2=NC=CC2=C1') # before fix
|
|
76
|
+
assert mol.thiele(fix_tautomers=True)
|
|
77
|
+
|
|
78
|
+
for n, m, bond in mol.bonds():
|
|
79
|
+
assert bond.order == 4
|
|
80
|
+
|
|
81
|
+
assert mol.atom(1).implicit_hydrogens == 0
|
|
82
|
+
assert mol.atom(5).implicit_hydrogens == 1
|
|
83
|
+
|
|
84
|
+
mol = smiles('N1C=CC2=NC=CC2=C1') # before fix
|
|
85
|
+
assert mol.thiele(fix_tautomers=False)
|
|
86
|
+
|
|
87
|
+
for n, m, bond in mol.bonds():
|
|
88
|
+
assert bond.order == 4
|
|
89
|
+
|
|
90
|
+
assert mol.atom(1).implicit_hydrogens == 1
|
|
91
|
+
assert mol.atom(5).implicit_hydrogens == 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_quinone_exclusion():
|
|
95
|
+
# Test that quinone-like structures are not aromatized
|
|
96
|
+
mol = smiles('O=C1C=CC(=O)C=C1') # para-benzoquinone
|
|
97
|
+
assert not mol.thiele() # should return False
|
|
98
|
+
assert mol == smiles('O=C1C=CC(=O)C=C1')
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_invalid_rings():
|
|
102
|
+
# Test that invalid rings are not aromatized
|
|
103
|
+
mol = smiles('C1=CC=C1') # cyclobutadiene - too small
|
|
104
|
+
assert not mol.thiele() # should return False
|
|
105
|
+
|
|
106
|
+
mol = smiles('C1=CC=CC=CC=CC=C1') # 10-membered ring - too large
|
|
107
|
+
assert not mol.thiele() # should return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_charged_systems():
|
|
111
|
+
# Test charged aromatic systems
|
|
112
|
+
mol = smiles('[NH+]1C=CC=CC=1') # pyridinium in Kekule form
|
|
113
|
+
assert mol.thiele()
|
|
114
|
+
|
|
115
|
+
# Check that charge is preserved and ring is aromatic
|
|
116
|
+
assert mol.atom(1).charge == 1
|
|
117
|
+
assert mol.atom(1).implicit_hydrogens == 1
|
|
118
|
+
|
|
119
|
+
for n, m, bond in mol.bonds():
|
|
120
|
+
assert bond.order == 4 # all bonds should be aromatic
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_freak_rules():
|
|
124
|
+
# Test special cases handled by freak rules
|
|
125
|
+
mol = smiles('N1C=CN2C=CC=C12') # special N-fused system in Kekule form
|
|
126
|
+
assert mol.thiele()
|
|
127
|
+
|
|
128
|
+
# Check that all bonds are aromatic
|
|
129
|
+
for n, m, bond in mol.bonds():
|
|
130
|
+
assert bond.order == 4
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_tetracyclic_systems():
|
|
134
|
+
# Test 4-membered ring in condensed systems
|
|
135
|
+
mol = smiles('C1=CC2=C(C=CC=C2)C1') # benzocyclobutene
|
|
136
|
+
assert mol.thiele()
|
|
137
|
+
assert mol == smiles('C1C=Cc2ccccc12')
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_phosphorus_rings():
|
|
141
|
+
# Test phosphorus-containing aromatic rings
|
|
142
|
+
mol = smiles('P1C=CC=CC=1') # phosphabenzene in Kekule form
|
|
143
|
+
assert mol.thiele()
|
|
144
|
+
|
|
145
|
+
# Check that all bonds in the ring are aromatic
|
|
146
|
+
for n, m, bond in mol.bonds():
|
|
147
|
+
assert bond.order == 4
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_boron_rings():
|
|
151
|
+
# Test boron-containing aromatic rings
|
|
152
|
+
mol = smiles('B1C=CC=C1') # borole in Kekule form
|
|
153
|
+
assert mol.thiele()
|
|
154
|
+
|
|
155
|
+
# Check that all bonds in the ring are aromatic
|
|
156
|
+
for n, m, bond in mol.bonds():
|
|
157
|
+
assert bond.order == 4
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_seven_membered_rings():
|
|
161
|
+
# Test 7-membered rings with different heteroatoms
|
|
162
|
+
# Only boron-containing 7-membered rings should be aromatic
|
|
163
|
+
mol = smiles('B1C=CC=CC=C1') # 7-membered ring with boron
|
|
164
|
+
assert mol.thiele()
|
|
165
|
+
|
|
166
|
+
mol = smiles('N1C=CC=CC=C1') # 7-membered ring with nitrogen
|
|
167
|
+
assert not mol.thiele()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_ferrocene_like():
|
|
171
|
+
# Test negatively charged carbon systems (ferrocene-like)
|
|
172
|
+
mol = smiles('[CH-]1C=CC=C1') # cyclopentadienyl anion
|
|
173
|
+
assert mol.thiele()
|
|
174
|
+
|
|
175
|
+
for n, m, bond in mol.bonds():
|
|
176
|
+
assert bond.order == 4
|
|
177
|
+
|
|
178
|
+
assert int(mol) == -1
|
|
179
|
+
assert mol.atom(1).charge == -1
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_multiple_components():
|
|
183
|
+
# Test systems with multiple aromatic components
|
|
184
|
+
mol = smiles('C1=CC=CC=C1.C1=CC=CC=C1') # two benzene molecules in Kekule form
|
|
185
|
+
assert mol.thiele()
|
|
186
|
+
|
|
187
|
+
# Check that all bonds in both components are aromatic
|
|
188
|
+
for n, m, bond in mol.bonds():
|
|
189
|
+
assert bond.order == 4
|
|
190
|
+
assert mol.connected_components_count == 2
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_complex_fused_systems():
|
|
194
|
+
# Test complex fused ring systems with multiple heteroatoms
|
|
195
|
+
|
|
196
|
+
# Benzothiazole (simplified)
|
|
197
|
+
mol = smiles('C1=CC=C2SC=NC2=C1') # benzothiazole in Kekule form
|
|
198
|
+
assert mol.thiele()
|
|
199
|
+
for n, m, bond in mol.bonds():
|
|
200
|
+
assert bond.order == 4
|
|
201
|
+
|
|
202
|
+
# Thienopyridine (simplified)
|
|
203
|
+
mol = smiles('C1=CC=NC2=CSC=C12') # thienopyridine in Kekule form
|
|
204
|
+
assert mol.thiele()
|
|
205
|
+
for n, m, bond in mol.bonds():
|
|
206
|
+
assert bond.order == 4
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_complex_charged_systems():
|
|
210
|
+
# Test charged aromatic systems
|
|
211
|
+
|
|
212
|
+
# Basic charged systems that should work
|
|
213
|
+
# Pyridinium
|
|
214
|
+
mol = smiles('[NH+]1C=CC=CC=1') # pyridinium in Kekule form
|
|
215
|
+
assert mol.thiele()
|
|
216
|
+
for n, m, bond in mol.bonds():
|
|
217
|
+
assert bond.order == 4
|
|
218
|
+
assert mol.atom(1).charge == 1
|
|
219
|
+
assert mol.atom(1).implicit_hydrogens == 1
|
|
220
|
+
|
|
221
|
+
# Complex charged systems that are not yet supported
|
|
222
|
+
# N-methylpyridinium (currently not aromatized properly)
|
|
223
|
+
mol = smiles('C[N+]1=CC=CC=C1') # N-methylpyridinium in Kekule form
|
|
224
|
+
assert mol.thiele()
|
|
225
|
+
assert mol.atom(2).charge == 1
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_complex_heterocycles():
|
|
229
|
+
# Test heterocyclic systems
|
|
230
|
+
|
|
231
|
+
# Basic heterocycles that should work
|
|
232
|
+
# Benzimidazole
|
|
233
|
+
mol = smiles('C1=CC=C2NC=NC2=C1') # in Kekule form
|
|
234
|
+
assert mol.thiele()
|
|
235
|
+
for n, m, bond in mol.bonds():
|
|
236
|
+
assert bond.order == 4
|
|
237
|
+
|
|
238
|
+
# Quinoxaline
|
|
239
|
+
mol = smiles('C1=CC=C2N=CC=NC2=C1') # in Kekule form
|
|
240
|
+
assert mol.thiele()
|
|
241
|
+
for n, m, bond in mol.bonds():
|
|
242
|
+
assert bond.order == 4
|
|
243
|
+
|
|
244
|
+
# Benzimidazole fused to thiophene (works)
|
|
245
|
+
mol = smiles('C1=CC2=C(C=C1)N=CN2C3=CC=CS3') # in Kekule form
|
|
246
|
+
assert mol.thiele()
|
|
247
|
+
|
|
248
|
+
for n, m, bond in mol.bonds():
|
|
249
|
+
# Check if both atoms of the bond are in the same ring
|
|
250
|
+
assert bond.order == 4 or (bond.order == 1 and n in (9, 10) and m in (9, 10))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_complex_bridged_systems():
|
|
254
|
+
# Complex bridged system with multiple heteroatoms (works)
|
|
255
|
+
mol = smiles('C1=CC2=C(C=C1)N=C3C(=C2)C=CC4=C3N=CS4') # in Kekule form
|
|
256
|
+
assert mol.thiele()
|
|
257
|
+
for n, m, bond in mol.bonds():
|
|
258
|
+
assert bond.order == 4
|
|
259
|
+
|
|
260
|
+
# Bridged system with N and S (works)
|
|
261
|
+
mol = smiles('C1=CC2=C(C=C1)SC3=C(N=CC=C3)C=C2') # in Kekule form
|
|
262
|
+
assert mol.thiele()
|
|
263
|
+
assert mol == smiles('S1c2ccccc2C=Cc2[n]cccc12')
|
|
@@ -0,0 +1,226 @@
|
|
|
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
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
from ._rules import freak_rules
|
|
22
|
+
from ..rings import _sssr, _connected_components
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from chython import MoleculeContainer
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# atomic number constants
|
|
30
|
+
B = 5
|
|
31
|
+
C = 6
|
|
32
|
+
N = 7
|
|
33
|
+
O = 8
|
|
34
|
+
P = 15
|
|
35
|
+
S = 16
|
|
36
|
+
Se = 34
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Thiele:
|
|
40
|
+
__slots__ = ()
|
|
41
|
+
|
|
42
|
+
def thiele(self: 'MoleculeContainer', *, fix_tautomers=True) -> bool:
|
|
43
|
+
"""
|
|
44
|
+
Convert structure to aromatic form (Huckel rule ignored). Return True if found any kekule ring.
|
|
45
|
+
Also marks atoms as aromatic.
|
|
46
|
+
|
|
47
|
+
:param fix_tautomers: try to fix condensed rings with pyrroles.
|
|
48
|
+
N1C=CC2=NC=CC2=C1>>N1C=CC2=CN=CC=C12
|
|
49
|
+
"""
|
|
50
|
+
atoms = self._atoms
|
|
51
|
+
bonds = self._bonds
|
|
52
|
+
nsc = self.not_special_connectivity
|
|
53
|
+
|
|
54
|
+
rings = defaultdict(set) # aromatic? skeleton. include quinones
|
|
55
|
+
tetracycles = []
|
|
56
|
+
pyrroles = set()
|
|
57
|
+
acceptors = set()
|
|
58
|
+
donors = []
|
|
59
|
+
freaks = []
|
|
60
|
+
for ring in self.sssr:
|
|
61
|
+
lr = len(ring)
|
|
62
|
+
if not 3 < lr < 8: # skip 3-membered and big rings
|
|
63
|
+
continue
|
|
64
|
+
# only B C N O P S with 2-3 neighbors. detects this: C1=CC=CP12=CC=CC=C2
|
|
65
|
+
if any(atoms[n] not in (C, N, O, S, B, P) or len(nsc[n]) > 3 for n in ring):
|
|
66
|
+
continue
|
|
67
|
+
sp2 = sum(atoms[n].hybridization == 2 for n in ring)
|
|
68
|
+
if sp2 == lr: # benzene like
|
|
69
|
+
if lr == 4: # two bonds condensed aromatic rings
|
|
70
|
+
tetracycles.append(ring)
|
|
71
|
+
else:
|
|
72
|
+
if fix_tautomers and lr % 2: # find potential pyrroles
|
|
73
|
+
acceptors.update(n for n in ring if (a := atoms[n]) == N and not a.charge)
|
|
74
|
+
n, *_, m = ring
|
|
75
|
+
rings[n].add(m)
|
|
76
|
+
rings[m].add(n)
|
|
77
|
+
for n, m in zip(ring, ring[1:]):
|
|
78
|
+
rings[n].add(m)
|
|
79
|
+
rings[m].add(n)
|
|
80
|
+
elif 4 < lr == sp2 + 1: # pyrroles, furanes, etc
|
|
81
|
+
try:
|
|
82
|
+
n = next(n for n in ring if atoms[n].hybridization == 1)
|
|
83
|
+
except StopIteration: # exotic, just skip
|
|
84
|
+
continue
|
|
85
|
+
if (a := atoms[n]).charge == -1:
|
|
86
|
+
if a != C or lr != 5: # skip any but ferrocene
|
|
87
|
+
continue
|
|
88
|
+
elif a.charge: # skip any charged
|
|
89
|
+
continue
|
|
90
|
+
elif lr == 7: # skip electron-rich 7-membered rings
|
|
91
|
+
if a != 5: # not B?
|
|
92
|
+
continue
|
|
93
|
+
# below lr == 5 or 6 only
|
|
94
|
+
elif a in (O, S, Se):
|
|
95
|
+
if len(bonds[n]) != 2: # like CS1(C)C=CC=C1
|
|
96
|
+
continue
|
|
97
|
+
elif a == N:
|
|
98
|
+
if (b := len(bonds[n])) > 3: # extra check for invalid N(IV)
|
|
99
|
+
continue
|
|
100
|
+
elif fix_tautomers and lr == 6 and b == 2:
|
|
101
|
+
donors.append(n)
|
|
102
|
+
elif a in (B, P):
|
|
103
|
+
if len(bonds[n]) > 3:
|
|
104
|
+
continue
|
|
105
|
+
else: # only B, [C-], N, O, P, S, Se
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
pyrroles.add(n)
|
|
109
|
+
n, *_, m = ring
|
|
110
|
+
rings[n].add(m)
|
|
111
|
+
rings[m].add(n)
|
|
112
|
+
for n, m in zip(ring, ring[1:]):
|
|
113
|
+
rings[n].add(m)
|
|
114
|
+
rings[m].add(n)
|
|
115
|
+
# like N1C=Cn2cccc12, S1C=Cn2cccc12
|
|
116
|
+
elif lr == 5 and sp2 == 3:
|
|
117
|
+
freaks.append(ring)
|
|
118
|
+
if not rings:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
# check out-of-ring double bonds
|
|
122
|
+
double_bonded = {n for n in rings if any(m not in rings[n] and b == 2 for m, b in bonds[n].items())}
|
|
123
|
+
|
|
124
|
+
# fix_tautomers
|
|
125
|
+
if fix_tautomers and acceptors and donors:
|
|
126
|
+
for start in donors:
|
|
127
|
+
stack = [(start, n, 0, 2) for n in rings[start] if n not in double_bonded]
|
|
128
|
+
path = []
|
|
129
|
+
seen = {start}
|
|
130
|
+
while stack:
|
|
131
|
+
last, current, depth, order = stack.pop()
|
|
132
|
+
if len(path) > depth:
|
|
133
|
+
seen.difference_update(x for _, x, _ in path[depth:])
|
|
134
|
+
path = path[:depth]
|
|
135
|
+
path.append((last, current, order))
|
|
136
|
+
if current in acceptors: # we found
|
|
137
|
+
if order == 1:
|
|
138
|
+
acceptors.discard(current)
|
|
139
|
+
pyrroles.discard(start)
|
|
140
|
+
pyrroles.add(current)
|
|
141
|
+
atoms[current]._implicit_hydrogens = 1
|
|
142
|
+
atoms[start]._implicit_hydrogens = 0
|
|
143
|
+
break
|
|
144
|
+
else:
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
depth += 1
|
|
148
|
+
seen.add(current)
|
|
149
|
+
new_order = 1 if order == 2 else 2
|
|
150
|
+
stack.extend((current, n, depth, new_order) for n in rings[current] if
|
|
151
|
+
n not in seen and n not in double_bonded and bonds[current][n] == order)
|
|
152
|
+
else: # path not found
|
|
153
|
+
continue
|
|
154
|
+
for n, m, o in path:
|
|
155
|
+
bonds[n][m]._order = o
|
|
156
|
+
if not acceptors:
|
|
157
|
+
break
|
|
158
|
+
self.flush_cache(keep_sssr=True, keep_components=True)
|
|
159
|
+
self.calc_labels()
|
|
160
|
+
|
|
161
|
+
if double_bonded: # delete quinones
|
|
162
|
+
for n in double_bonded:
|
|
163
|
+
for m in rings.pop(n):
|
|
164
|
+
rings[m].discard(n)
|
|
165
|
+
|
|
166
|
+
for n in [n for n, ms in rings.items() if not ms]: # imide leads to isolated atoms
|
|
167
|
+
del rings[n]
|
|
168
|
+
if not rings:
|
|
169
|
+
return False
|
|
170
|
+
while True:
|
|
171
|
+
try:
|
|
172
|
+
n = next(n for n, ms in rings.items() if len(ms) == 1)
|
|
173
|
+
except StopIteration:
|
|
174
|
+
break
|
|
175
|
+
m = rings.pop(n).pop()
|
|
176
|
+
if n in pyrroles:
|
|
177
|
+
rings[m].discard(n)
|
|
178
|
+
else:
|
|
179
|
+
pm = rings.pop(m)
|
|
180
|
+
pm.discard(n)
|
|
181
|
+
for x in pm:
|
|
182
|
+
rings[x].discard(m)
|
|
183
|
+
if not rings:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
n_sssr = sum(len(x) for x in rings.values()) // 2 - len(rings) + len(_connected_components(rings))
|
|
187
|
+
if not n_sssr:
|
|
188
|
+
return False
|
|
189
|
+
rings = _sssr(rings, n_sssr) # search rings again
|
|
190
|
+
|
|
191
|
+
seen = set()
|
|
192
|
+
for ring in rings:
|
|
193
|
+
seen.update(ring)
|
|
194
|
+
|
|
195
|
+
# reset bonds to single
|
|
196
|
+
for ring in tetracycles:
|
|
197
|
+
if seen.issuperset(ring):
|
|
198
|
+
n, *_, m = ring
|
|
199
|
+
bonds[n][m]._order = 1
|
|
200
|
+
for n, m in zip(ring, ring[1:]):
|
|
201
|
+
bonds[n][m]._order = 1
|
|
202
|
+
|
|
203
|
+
for ring in rings:
|
|
204
|
+
n, *_, m = ring
|
|
205
|
+
bonds[n][m]._order = 4
|
|
206
|
+
for n, m in zip(ring, ring[1:]):
|
|
207
|
+
bonds[n][m]._order = 4
|
|
208
|
+
|
|
209
|
+
self.flush_cache(keep_sssr=True, keep_components=True)
|
|
210
|
+
self.calc_labels()
|
|
211
|
+
for ring in freaks: # aromatize rule based
|
|
212
|
+
for q in freak_rules:
|
|
213
|
+
if next(q.get_mapping(self, searching_scope=ring, automorphism_filter=False), None):
|
|
214
|
+
n, *_, m = ring
|
|
215
|
+
bonds[n][m]._order = 4
|
|
216
|
+
for n, m in zip(ring, ring[1:]):
|
|
217
|
+
bonds[n][m]._order = 4
|
|
218
|
+
break
|
|
219
|
+
if freaks:
|
|
220
|
+
self.flush_cache(keep_sssr=True, keep_components=True) # flush again
|
|
221
|
+
self.calc_labels()
|
|
222
|
+
self.fix_stereo() # check if any stereo centers vanished.
|
|
223
|
+
return True
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
__all__ = ['Thiele']
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2019-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
|
+
#
|
|
19
|
+
from .molecule import *
|
|
20
|
+
from .reaction import *
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ['Calculate2DMolecule', 'Calculate2DReaction']
|