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.
Files changed (171) hide show
  1. chython/__init__.py +31 -0
  2. chython/_functions.py +69 -0
  3. chython/algorithms/__init__.py +18 -0
  4. chython/algorithms/_isomorphism.cp312-win_amd64.pyd +0 -0
  5. chython/algorithms/_isomorphism.pyx +189 -0
  6. chython/algorithms/aromatics/__init__.py +27 -0
  7. chython/algorithms/aromatics/_rules.py +124 -0
  8. chython/algorithms/aromatics/kekule.py +530 -0
  9. chython/algorithms/aromatics/test/__init__.py +18 -0
  10. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  11. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  12. chython/algorithms/aromatics/thiele.py +226 -0
  13. chython/algorithms/calculate2d/__init__.py +23 -0
  14. chython/algorithms/calculate2d/clean2d.js +1 -0
  15. chython/algorithms/calculate2d/molecule.py +149 -0
  16. chython/algorithms/calculate2d/reaction.py +80 -0
  17. chython/algorithms/depict.py +523 -0
  18. chython/algorithms/fingerprints/__init__.py +48 -0
  19. chython/algorithms/fingerprints/linear.py +208 -0
  20. chython/algorithms/fingerprints/morgan.py +137 -0
  21. chython/algorithms/fingerprints/test/__init__.py +18 -0
  22. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  23. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  24. chython/algorithms/isomorphism.py +667 -0
  25. chython/algorithms/mapping/__init__.py +28 -0
  26. chython/algorithms/mapping/_groups.py +94 -0
  27. chython/algorithms/mapping/_reactions.py +72 -0
  28. chython/algorithms/mapping/attention.py +216 -0
  29. chython/algorithms/mapping/fixmapper.py +84 -0
  30. chython/algorithms/mapping/groups.py +137 -0
  31. chython/algorithms/mcs.py +202 -0
  32. chython/algorithms/morgan.py +82 -0
  33. chython/algorithms/rings.py +551 -0
  34. chython/algorithms/smiles.py +556 -0
  35. chython/algorithms/standardize/__init__.py +30 -0
  36. chython/algorithms/standardize/_charged.py +115 -0
  37. chython/algorithms/standardize/_groups.py +926 -0
  38. chython/algorithms/standardize/_metal_organics.py +189 -0
  39. chython/algorithms/standardize/_reagents.py +44 -0
  40. chython/algorithms/standardize/_salts.py +68 -0
  41. chython/algorithms/standardize/molecule.py +479 -0
  42. chython/algorithms/standardize/reaction.py +429 -0
  43. chython/algorithms/standardize/resonance.py +196 -0
  44. chython/algorithms/standardize/salts.py +128 -0
  45. chython/algorithms/standardize/saturation.py +378 -0
  46. chython/algorithms/standardize/test/__init__.py +18 -0
  47. chython/algorithms/standardize/test/test_groups.py +118 -0
  48. chython/algorithms/stereo.py +1222 -0
  49. chython/algorithms/tautomers/__init__.py +171 -0
  50. chython/algorithms/tautomers/_acid.py +59 -0
  51. chython/algorithms/tautomers/_base.py +116 -0
  52. chython/algorithms/tautomers/_keto_enol.py +107 -0
  53. chython/algorithms/tautomers/acid_base.py +214 -0
  54. chython/algorithms/tautomers/heteroarenes.py +108 -0
  55. chython/algorithms/tautomers/keto_enol.py +148 -0
  56. chython/algorithms/tautomers/test/__init__.py +18 -0
  57. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  58. chython/algorithms/test/__init__.py +18 -0
  59. chython/algorithms/test/test_isomorphism.py +58 -0
  60. chython/algorithms/test/test_smiles.py +76 -0
  61. chython/algorithms/x3dom.py +355 -0
  62. chython/containers/__init__.py +43 -0
  63. chython/containers/_pack_v2.cp312-win_amd64.pyd +0 -0
  64. chython/containers/_pack_v2.pyx +271 -0
  65. chython/containers/_unpack_v0v2.cp312-win_amd64.pyd +0 -0
  66. chython/containers/_unpack_v0v2.pyx +300 -0
  67. chython/containers/bonds.py +255 -0
  68. chython/containers/cgr.py +115 -0
  69. chython/containers/graph.py +200 -0
  70. chython/containers/molecule.py +695 -0
  71. chython/containers/query.py +66 -0
  72. chython/containers/reaction.py +324 -0
  73. chython/exceptions.py +114 -0
  74. chython/files/MRVrw.py +523 -0
  75. chython/files/PDBrw.py +257 -0
  76. chython/files/RDFrw.py +298 -0
  77. chython/files/SDFrw.py +221 -0
  78. chython/files/__init__.py +29 -0
  79. chython/files/_convert.py +207 -0
  80. chython/files/_mapping.py +117 -0
  81. chython/files/_xyz.cp312-win_amd64.pyd +0 -0
  82. chython/files/_xyz.pyx +74 -0
  83. chython/files/daylight/__init__.py +24 -0
  84. chython/files/daylight/parser.py +157 -0
  85. chython/files/daylight/smarts.py +107 -0
  86. chython/files/daylight/smiles.py +245 -0
  87. chython/files/daylight/test/__init__.py +18 -0
  88. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  89. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  90. chython/files/daylight/test/test_parser.py +118 -0
  91. chython/files/daylight/test/test_tokenize.py +77 -0
  92. chython/files/daylight/tokenize.py +376 -0
  93. chython/files/libinchi/__init__.py +22 -0
  94. chython/files/libinchi/libinchi.dll +0 -0
  95. chython/files/libinchi/wrapper.py +555 -0
  96. chython/files/mdl/__init__.py +25 -0
  97. chython/files/mdl/emol.py +209 -0
  98. chython/files/mdl/erxn.py +67 -0
  99. chython/files/mdl/mol.py +144 -0
  100. chython/files/mdl/read.py +225 -0
  101. chython/files/mdl/rxn.py +67 -0
  102. chython/files/mdl/stereo.py +63 -0
  103. chython/files/mdl/write.py +165 -0
  104. chython/files/xyz.py +78 -0
  105. chython/periodictable/__init__.py +69 -0
  106. chython/periodictable/base/__init__.py +25 -0
  107. chython/periodictable/base/dynamic.py +149 -0
  108. chython/periodictable/base/element.py +452 -0
  109. chython/periodictable/base/groups.py +90 -0
  110. chython/periodictable/base/periods.py +46 -0
  111. chython/periodictable/base/query.py +473 -0
  112. chython/periodictable/base/vector.py +119 -0
  113. chython/periodictable/groupI.py +252 -0
  114. chython/periodictable/groupII.py +223 -0
  115. chython/periodictable/groupIII.py +1154 -0
  116. chython/periodictable/groupIV.py +210 -0
  117. chython/periodictable/groupIX.py +196 -0
  118. chython/periodictable/groupV.py +192 -0
  119. chython/periodictable/groupVI.py +183 -0
  120. chython/periodictable/groupVII.py +162 -0
  121. chython/periodictable/groupVIII.py +161 -0
  122. chython/periodictable/groupX.py +166 -0
  123. chython/periodictable/groupXI.py +164 -0
  124. chython/periodictable/groupXII.py +158 -0
  125. chython/periodictable/groupXIII.py +239 -0
  126. chython/periodictable/groupXIV.py +262 -0
  127. chython/periodictable/groupXV.py +276 -0
  128. chython/periodictable/groupXVI.py +457 -0
  129. chython/periodictable/groupXVII.py +324 -0
  130. chython/periodictable/groupXVIII.py +260 -0
  131. chython/reactor/__init__.py +24 -0
  132. chython/reactor/base.py +212 -0
  133. chython/reactor/deprotection.py +565 -0
  134. chython/reactor/groups.py +128 -0
  135. chython/reactor/reactions/__init__.py +157 -0
  136. chython/reactor/reactions/_amidation.py +58 -0
  137. chython/reactor/reactions/_amine_isocyanate.py +52 -0
  138. chython/reactor/reactions/_buchwald_hartwig.py +50 -0
  139. chython/reactor/reactions/_esterification.py +49 -0
  140. chython/reactor/reactions/_macmillan.py +43 -0
  141. chython/reactor/reactions/_reductive_amination.py +50 -0
  142. chython/reactor/reactions/_sonogashira.py +49 -0
  143. chython/reactor/reactions/_sulfonamidation.py +52 -0
  144. chython/reactor/reactions/_suzuki_miyaura.py +88 -0
  145. chython/reactor/reactions/ufe.py +99 -0
  146. chython/reactor/reactor.py +170 -0
  147. chython/reactor/retro/__init__.py +77 -0
  148. chython/reactor/retro/_amidation.py +39 -0
  149. chython/reactor/retro/_aryl_amination.py +32 -0
  150. chython/reactor/retro/_mitsunobu.py +49 -0
  151. chython/reactor/retro/_sonogashira.py +49 -0
  152. chython/reactor/retro/_suzuki_miyaura.py +54 -0
  153. chython/reactor/scaffold.py +122 -0
  154. chython/reactor/test/__init__.py +18 -0
  155. chython/reactor/test/test_deprotection.py +57 -0
  156. chython/reactor/test/test_reactor.py +40 -0
  157. chython/reactor/test/test_scaffold.py +57 -0
  158. chython/reactor/test/test_transformer.py +40 -0
  159. chython/reactor/transformer.py +62 -0
  160. chython/utils/__init__.py +57 -0
  161. chython/utils/free_wilson.py +114 -0
  162. chython/utils/grid.py +130 -0
  163. chython/utils/rdkit.py +165 -0
  164. chython/utils/retro.py +139 -0
  165. chython/utils/svg.py +57 -0
  166. chython/utils/test/__init__.py +18 -0
  167. chython/utils/test/test_rdkit.py +71 -0
  168. chython-2.4.dist-info/LICENSE +165 -0
  169. chython-2.4.dist-info/METADATA +101 -0
  170. chython-2.4.dist-info/RECORD +171 -0
  171. 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']