chython 2.19__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chython/__init__.py +35 -0
- chython/_functions.py +95 -0
- chython/_java.py +57 -0
- chython/algorithms/__init__.py +18 -0
- chython/algorithms/_isomorphism.cp313-win_amd64.pyd +0 -0
- chython/algorithms/_isomorphism.pyx +189 -0
- chython/algorithms/_rings.cp313-win_amd64.pyd +0 -0
- chython/algorithms/_rings.pyx +478 -0
- chython/algorithms/aromatics/__init__.py +27 -0
- chython/algorithms/aromatics/_rules.py +124 -0
- chython/algorithms/aromatics/kekule.py +532 -0
- chython/algorithms/aromatics/test/__init__.py +18 -0
- chython/algorithms/aromatics/test/test_kekule.py +160 -0
- chython/algorithms/aromatics/test/test_thiele.py +263 -0
- chython/algorithms/aromatics/thiele.py +222 -0
- chython/algorithms/calculate2d/__init__.py +23 -0
- chython/algorithms/calculate2d/clean2d.js +1 -0
- chython/algorithms/calculate2d/molecule.py +191 -0
- chython/algorithms/calculate2d/reaction.py +75 -0
- chython/algorithms/conformers.py +106 -0
- chython/algorithms/depict.py +596 -0
- chython/algorithms/fingerprints/__init__.py +43 -0
- chython/algorithms/fingerprints/linear.py +202 -0
- chython/algorithms/fingerprints/morgan.py +132 -0
- chython/algorithms/fingerprints/test/__init__.py +18 -0
- chython/algorithms/fingerprints/test/test_linear.py +127 -0
- chython/algorithms/fingerprints/test/test_morgan.py +79 -0
- chython/algorithms/groups/__init__.py +212 -0
- chython/algorithms/groups/_functional.py +290 -0
- chython/algorithms/groups/_oxidations.py +70 -0
- chython/algorithms/groups/_protective.py +964 -0
- chython/algorithms/groups/_reactions.py +809 -0
- chython/algorithms/groups/_reductions.py +88 -0
- chython/algorithms/groups/_transformations.py +278 -0
- chython/algorithms/groups/test/__init__.py +18 -0
- chython/algorithms/groups/test/test_deprotection.py +108 -0
- chython/algorithms/groups/test/test_reactions.py +377 -0
- chython/algorithms/isomorphism.py +664 -0
- chython/algorithms/mapping/__init__.py +188 -0
- chython/algorithms/mapping/_groups.py +94 -0
- chython/algorithms/mapping/_reactions.py +72 -0
- chython/algorithms/mapping/attention.py +266 -0
- chython/algorithms/mapping/reconstruct.py +346 -0
- chython/algorithms/mcs.py +201 -0
- chython/algorithms/morgan.py +77 -0
- chython/algorithms/rings.py +178 -0
- chython/algorithms/smiles.py +597 -0
- chython/algorithms/standardize/__init__.py +30 -0
- chython/algorithms/standardize/_groups.py +958 -0
- chython/algorithms/standardize/_isomers.py +330 -0
- chython/algorithms/standardize/_metal_organics.py +189 -0
- chython/algorithms/standardize/_reagents.py +44 -0
- chython/algorithms/standardize/_salts.py +68 -0
- chython/algorithms/standardize/molecule.py +475 -0
- chython/algorithms/standardize/reaction.py +384 -0
- chython/algorithms/standardize/resonance.py +191 -0
- chython/algorithms/standardize/salts.py +128 -0
- chython/algorithms/standardize/saturation.py +373 -0
- chython/algorithms/standardize/test/__init__.py +18 -0
- chython/algorithms/standardize/test/test_canonicalize.py +66 -0
- chython/algorithms/standardize/test/test_groups.py +146 -0
- chython/algorithms/standardize/test/test_isomers.py +198 -0
- chython/algorithms/stereo.py +1215 -0
- chython/algorithms/tautomers/__init__.py +167 -0
- chython/algorithms/tautomers/_acid.py +59 -0
- chython/algorithms/tautomers/_base.py +116 -0
- chython/algorithms/tautomers/_keto_enol.py +107 -0
- chython/algorithms/tautomers/acid_base.py +208 -0
- chython/algorithms/tautomers/heteroarenes.py +103 -0
- chython/algorithms/tautomers/keto_enol.py +143 -0
- chython/algorithms/tautomers/test/__init__.py +18 -0
- chython/algorithms/tautomers/test/test_tautomers.py +80 -0
- chython/algorithms/test/__init__.py +18 -0
- chython/algorithms/test/test_isomorphism.py +58 -0
- chython/algorithms/test/test_smiles.py +76 -0
- chython/algorithms/x3dom.py +350 -0
- chython/containers/__init__.py +48 -0
- chython/containers/_pack_v2.cp313-win_amd64.pyd +0 -0
- chython/containers/_pack_v2.pyx +271 -0
- chython/containers/_unpack_v0v2.cp313-win_amd64.pyd +0 -0
- chython/containers/_unpack_v0v2.pyx +302 -0
- chython/containers/bonds.py +255 -0
- chython/containers/cgr.py +115 -0
- chython/containers/chimera.py +63 -0
- chython/containers/graph.py +199 -0
- chython/containers/molecule.py +817 -0
- chython/containers/query.py +71 -0
- chython/containers/rdkit.py +183 -0
- chython/containers/reaction.py +324 -0
- chython/containers/test/__init__.py +18 -0
- chython/containers/test/test_molecule.py +117 -0
- chython/containers/test/test_pack.py +60 -0
- chython/containers/test/test_rdkit.py +70 -0
- chython/containers/test/test_reaction.py +40 -0
- chython/exceptions.py +114 -0
- chython/files/MRVrw.py +532 -0
- chython/files/PDBrw.py +255 -0
- chython/files/RDFrw.py +309 -0
- chython/files/SDFrw.py +221 -0
- chython/files/__init__.py +30 -0
- chython/files/_convert.py +207 -0
- chython/files/_mapping.py +117 -0
- chython/files/_xyz.cp313-win_amd64.pyd +0 -0
- chython/files/_xyz.pyx +74 -0
- chython/files/daylight/__init__.py +24 -0
- chython/files/daylight/parser.py +157 -0
- chython/files/daylight/smarts.py +107 -0
- chython/files/daylight/smiles.py +260 -0
- chython/files/daylight/test/__init__.py +18 -0
- chython/files/daylight/test/test_daylight_smarts.py +289 -0
- chython/files/daylight/test/test_daylight_smiles.py +101 -0
- chython/files/daylight/test/test_parser.py +118 -0
- chython/files/daylight/test/test_tokenize.py +77 -0
- chython/files/daylight/tokenize.py +376 -0
- chython/files/libinchi/__init__.py +22 -0
- chython/files/libinchi/libinchi.dll +0 -0
- chython/files/libinchi/wrapper.py +554 -0
- chython/files/mdl/__init__.py +25 -0
- chython/files/mdl/emol.py +251 -0
- chython/files/mdl/erxn.py +67 -0
- chython/files/mdl/mol.py +186 -0
- chython/files/mdl/read.py +223 -0
- chython/files/mdl/rxn.py +67 -0
- chython/files/mdl/stereo.py +63 -0
- chython/files/mdl/write.py +201 -0
- chython/files/opsin.py +35 -0
- chython/files/test/__init__.py +0 -0
- chython/files/test/test_inchi.py +27 -0
- chython/files/test/test_mrv.py +47 -0
- chython/files/test/test_sdf_rdf.py +64 -0
- chython/files/test/test_xyz.py +20 -0
- chython/files/xyz.py +78 -0
- chython/periodictable/__init__.py +69 -0
- chython/periodictable/base/__init__.py +25 -0
- chython/periodictable/base/dynamic.py +149 -0
- chython/periodictable/base/element.py +451 -0
- chython/periodictable/base/groups.py +90 -0
- chython/periodictable/base/periods.py +46 -0
- chython/periodictable/base/query.py +473 -0
- chython/periodictable/base/vector.py +119 -0
- chython/periodictable/groupI.py +252 -0
- chython/periodictable/groupII.py +223 -0
- chython/periodictable/groupIII.py +1154 -0
- chython/periodictable/groupIV.py +210 -0
- chython/periodictable/groupIX.py +196 -0
- chython/periodictable/groupV.py +192 -0
- chython/periodictable/groupVI.py +183 -0
- chython/periodictable/groupVII.py +162 -0
- chython/periodictable/groupVIII.py +161 -0
- chython/periodictable/groupX.py +166 -0
- chython/periodictable/groupXI.py +164 -0
- chython/periodictable/groupXII.py +158 -0
- chython/periodictable/groupXIII.py +239 -0
- chython/periodictable/groupXIV.py +262 -0
- chython/periodictable/groupXV.py +276 -0
- chython/periodictable/groupXVI.py +457 -0
- chython/periodictable/groupXVII.py +324 -0
- chython/periodictable/groupXVIII.py +260 -0
- chython/periodictable/test/__init__.py +0 -0
- chython/periodictable/test/test_element.py +100 -0
- chython/reactor/__init__.py +24 -0
- chython/reactor/base.py +222 -0
- chython/reactor/reactor.py +177 -0
- chython/reactor/test/__init__.py +18 -0
- chython/reactor/test/test_reactor.py +40 -0
- chython/reactor/test/test_transformer.py +78 -0
- chython/reactor/transformer.py +72 -0
- chython/utils/__init__.py +50 -0
- chython/utils/free_wilson.py +114 -0
- chython/utils/grid.py +130 -0
- chython/utils/retro.py +139 -0
- chython-2.19.dist-info/METADATA +105 -0
- chython-2.19.dist-info/RECORD +175 -0
- chython-2.19.dist-info/WHEEL +4 -0
- chython-2.19.dist-info/licenses/LICENSE +165 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2021-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# This file is part of chython.
|
|
5
|
+
#
|
|
6
|
+
# chython is free software; you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
8
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU Lesser General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
17
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
#
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
from itertools import product
|
|
21
|
+
from operator import itemgetter
|
|
22
|
+
from random import shuffle
|
|
23
|
+
from ...containers.bonds import Bond
|
|
24
|
+
from ...exceptions import ValenceError
|
|
25
|
+
|
|
26
|
+
# atom, charge, unsaturation
|
|
27
|
+
tuned_priority = {(7, 0, 0): -3, # amine
|
|
28
|
+
(7, 0, 1): -3, # X=N-X
|
|
29
|
+
(7, 0, 2): -3, # X#N
|
|
30
|
+
(7, 1, 1): -2, # X=[N+](-X)-X
|
|
31
|
+
(7, 1, 2): -2, # X=[N+]=X
|
|
32
|
+
(7, -1, 0): -1, # X-[N-]-X
|
|
33
|
+
(7, -1, 1): -1, # X=[N-]
|
|
34
|
+
(8, 0, 0): -2, # X-O-X
|
|
35
|
+
(8, 0, 1): -2, # X=O
|
|
36
|
+
(8, -1, 0): -1, # X-[O-]
|
|
37
|
+
(8, 1, 1): -1, # X=[O+]-X
|
|
38
|
+
(16, 0, 0): -3, # X-S-X
|
|
39
|
+
(16, 0, 2): -2, # X-S(-X)(=X)=X
|
|
40
|
+
(16, 0, 1): -2, # X-S(-X)(=X)
|
|
41
|
+
(16, 0, 2): -2, # X=S=X
|
|
42
|
+
(16, 0, 0): -1, # X-S(-X)(-X)-X
|
|
43
|
+
(16, 0, 1): -1, # X-[S+](-X)(-X)
|
|
44
|
+
(16, 1, 1): -1, # X=[S+]-X
|
|
45
|
+
}
|
|
46
|
+
charge_priority = {0: 0, -1: 1, 1: 2, 2: 3, 3: 4, -2: 5, 4: 6, -3: 7, -4: 8}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Saturation:
|
|
50
|
+
__slots__ = ()
|
|
51
|
+
|
|
52
|
+
def saturate(self, neighbors_distances: dict[int, dict[int, float]] | None = None,
|
|
53
|
+
reset_electrons: bool = True, expected_charge: int = 0, expected_radicals_count: int = 0,
|
|
54
|
+
allow_errors: bool = True, logging: bool = False) -> bool | list:
|
|
55
|
+
"""
|
|
56
|
+
Saturate molecules with double and triple bonds and charges and radical states to correct valences of atoms.
|
|
57
|
+
Note: works only with fully explicit hydrogens!
|
|
58
|
+
|
|
59
|
+
:param neighbors_distances: If given longest bonds can be removed if need.
|
|
60
|
+
:param reset_electrons: Can change charges and radicals if need.
|
|
61
|
+
:param expected_charge: Reset charge to given. Works only with reset_electrons=True.
|
|
62
|
+
:param expected_radicals_count: Reset radical atoms count to given. Works only with reset_electrons=True.
|
|
63
|
+
:param allow_errors: allow unbalanced result.
|
|
64
|
+
:param logging: return log.
|
|
65
|
+
"""
|
|
66
|
+
if any(b != 1 for _, _, b in self.bonds()):
|
|
67
|
+
raise ValenceError('only single bonded skeleton can be saturated')
|
|
68
|
+
atoms = self._atoms
|
|
69
|
+
if not reset_electrons:
|
|
70
|
+
expected_radicals_count = sum(a.is_radical for a in atoms.values())
|
|
71
|
+
expected_charge = int(self)
|
|
72
|
+
|
|
73
|
+
if reset_electrons:
|
|
74
|
+
charges = {x: None for x in self}
|
|
75
|
+
radicals = {x: None for x in self}
|
|
76
|
+
else:
|
|
77
|
+
charges = {n: a.charge for n, a in self.atoms()}
|
|
78
|
+
radicals = {n: a.is_radical for n, a in self.atoms()}
|
|
79
|
+
sat, adjacency = _find_possible_valences(atoms, neighbors_distances or self._bonds,
|
|
80
|
+
charges, radicals, neighbors_distances is not None)
|
|
81
|
+
charges = {} # new charge states
|
|
82
|
+
radicals = {} # new radical states
|
|
83
|
+
bonds = {n: {} for n in atoms} # new bonds
|
|
84
|
+
|
|
85
|
+
seen = set()
|
|
86
|
+
unsaturated = {}
|
|
87
|
+
for n, env in adjacency.items(): # set single bonds in molecule. collect unsaturated atoms
|
|
88
|
+
s = sat[n]
|
|
89
|
+
if len(s) == 1:
|
|
90
|
+
c, r, h = s.pop()
|
|
91
|
+
if not h:
|
|
92
|
+
seen.add(n)
|
|
93
|
+
charges[n] = c
|
|
94
|
+
radicals[n] = r
|
|
95
|
+
for m in env:
|
|
96
|
+
if m not in seen:
|
|
97
|
+
bonds[n][m] = bonds[m][n] = Bond(1)
|
|
98
|
+
else:
|
|
99
|
+
unsaturated[n] = [(c, r, h)]
|
|
100
|
+
else:
|
|
101
|
+
# radicals have the lowest priority
|
|
102
|
+
# tuned priority
|
|
103
|
+
# multiple bonds have higher priority
|
|
104
|
+
# charges priority: 0>-1>1>2>3>-2>4>-3>-4
|
|
105
|
+
unsaturated[n] = sorted(s, key=lambda x: (x[1],
|
|
106
|
+
tuned_priority.get((atoms[n].atomic_number, x[0], x[2]), 0),
|
|
107
|
+
-x[2], charge_priority[x[0]]))
|
|
108
|
+
|
|
109
|
+
log = []
|
|
110
|
+
if (need_radicals := expected_radicals_count - sum(radicals.values())) < 0:
|
|
111
|
+
log.append('Radical state not balanced')
|
|
112
|
+
if not allow_errors:
|
|
113
|
+
if logging:
|
|
114
|
+
return log
|
|
115
|
+
return False
|
|
116
|
+
need_radicals = 0 # reset to zero
|
|
117
|
+
|
|
118
|
+
if unsaturated:
|
|
119
|
+
# create graph of unsaturated atoms
|
|
120
|
+
bonds_graph = {n: {m for m in adjacency[n] if m in unsaturated} for n in unsaturated}
|
|
121
|
+
order = list(unsaturated)
|
|
122
|
+
# try to saturate with different random states
|
|
123
|
+
for _ in range(len(unsaturated)):
|
|
124
|
+
shuffle(order)
|
|
125
|
+
sb, sa, log_ = _saturate({n: bonds_graph[n].copy() for n in order}, unsaturated, need_radicals,
|
|
126
|
+
expected_charge - sum(charges.values()))
|
|
127
|
+
if not log_: # success
|
|
128
|
+
break
|
|
129
|
+
else: # failed
|
|
130
|
+
if log_ == 1:
|
|
131
|
+
log.append('Charge state not balanced')
|
|
132
|
+
elif log_ == 2:
|
|
133
|
+
log.append('Radical state not balanced')
|
|
134
|
+
else:
|
|
135
|
+
log.append('Charge state not balanced')
|
|
136
|
+
log.append('Radical state not balanced')
|
|
137
|
+
if not allow_errors: # all attempts failed
|
|
138
|
+
if logging:
|
|
139
|
+
return log
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
for n, m, b in sb:
|
|
143
|
+
bonds[n][m] = bonds[m][n] = Bond(b)
|
|
144
|
+
for n, c, r in sa:
|
|
145
|
+
charges[n] = c
|
|
146
|
+
radicals[n] = r
|
|
147
|
+
elif expected_charge != sum(charges.values()): # check charge for saturated case
|
|
148
|
+
log.append('Charge state not balanced')
|
|
149
|
+
if not allow_errors:
|
|
150
|
+
if logging:
|
|
151
|
+
return log
|
|
152
|
+
return False
|
|
153
|
+
# reset molecule
|
|
154
|
+
self._bonds = bonds
|
|
155
|
+
for n, r in radicals.items():
|
|
156
|
+
atoms[n]._is_radical = r
|
|
157
|
+
for n, c in charges.items():
|
|
158
|
+
atoms[n]._charge = c
|
|
159
|
+
for a in atoms.values():
|
|
160
|
+
a._implicit_hydrogens = 0 # reset invalid hydrogens counts.
|
|
161
|
+
self.flush_cache()
|
|
162
|
+
self.calc_labels()
|
|
163
|
+
if logging:
|
|
164
|
+
if not log: # check for errors
|
|
165
|
+
log.append('Saturated successfully')
|
|
166
|
+
else:
|
|
167
|
+
log.append('Saturated with errors')
|
|
168
|
+
return log
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _find_possible_valences(atoms, neighbors_distances, charges, radicals, allow_deleting=True):
|
|
173
|
+
if allow_deleting:
|
|
174
|
+
possible_bonds = {n: md.copy() for n, md in neighbors_distances.items()}
|
|
175
|
+
else:
|
|
176
|
+
possible_bonds = {n: list(md) for n, md in neighbors_distances.items()}
|
|
177
|
+
while True:
|
|
178
|
+
saturation = defaultdict(set)
|
|
179
|
+
for n, env in possible_bonds.items():
|
|
180
|
+
env_atoms = None
|
|
181
|
+
el = len(env)
|
|
182
|
+
dc = charges[n]
|
|
183
|
+
dr = radicals[n]
|
|
184
|
+
for charge, is_radical, valence, implicit, explicit_dict in atoms[n].saturation_rules:
|
|
185
|
+
if valence < el or dc is not None and dc != charge or dr is not None and dr != is_radical:
|
|
186
|
+
continue # skip impossible rules
|
|
187
|
+
if explicit_dict:
|
|
188
|
+
if env_atoms is None: # lazy caching
|
|
189
|
+
env_atoms = defaultdict(int)
|
|
190
|
+
for m in env:
|
|
191
|
+
env_atoms[atoms[m].atomic_number] += 1
|
|
192
|
+
env_atoms_copy = env_atoms.copy()
|
|
193
|
+
for (b, a), c in explicit_dict.items(): # stage 1. find explicit valence
|
|
194
|
+
if env_atoms_copy[a] < c: # `c` always > 0
|
|
195
|
+
break # rule not matched
|
|
196
|
+
env_atoms_copy[a] -= c
|
|
197
|
+
else: # stage 2. find possible valence
|
|
198
|
+
if unmatched := sum(env_atoms_copy.values()): # number of atoms outside rule
|
|
199
|
+
if implicit >= unmatched:
|
|
200
|
+
# number of implicit H should be greater or equal to number of neighbors
|
|
201
|
+
saturation[n].add((charge, is_radical, valence - el))
|
|
202
|
+
else: # pattern fully matched. difference bw valence and connectivity is unsaturation.
|
|
203
|
+
saturation[n].add((charge, is_radical, valence - el))
|
|
204
|
+
else: # unspecific rule. found possible valence
|
|
205
|
+
saturation[n].add((charge, is_radical, valence - el))
|
|
206
|
+
if n not in saturation: # valence not found
|
|
207
|
+
break
|
|
208
|
+
else: # all atoms passed
|
|
209
|
+
break
|
|
210
|
+
if allow_deleting:
|
|
211
|
+
out = max(env.items(), key=itemgetter(1))[0]
|
|
212
|
+
del possible_bonds[out][n]
|
|
213
|
+
del possible_bonds[n][out]
|
|
214
|
+
else:
|
|
215
|
+
raise ValenceError('Structure has invalid atoms neighbors count and electron states')
|
|
216
|
+
return saturation, possible_bonds
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _saturate(bonds, atoms, expected_radicals_count, expected_charge):
|
|
220
|
+
atoms = {k: v.copy() for k, v in atoms.items()}
|
|
221
|
+
dots = []
|
|
222
|
+
saturation = []
|
|
223
|
+
electrons = []
|
|
224
|
+
while True:
|
|
225
|
+
# get isolated atoms. atoms should be charged or radical
|
|
226
|
+
to_del = []
|
|
227
|
+
for n, env in bonds.items():
|
|
228
|
+
if not env:
|
|
229
|
+
es = [(n, c, r) for c, r, h in atoms[n] if not h]
|
|
230
|
+
if not es:
|
|
231
|
+
raise ValenceError('Saturation impossible. '
|
|
232
|
+
f"Isolated atom ({n}) doesn't have appropriate charge-radical state")
|
|
233
|
+
to_del.append(n)
|
|
234
|
+
dots.append(es)
|
|
235
|
+
for n in to_del:
|
|
236
|
+
del bonds[n]
|
|
237
|
+
if not bonds:
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
try: # get terminal atom
|
|
241
|
+
n = next(n for n, ms in bonds.items() if len(ms) == 1)
|
|
242
|
+
except StopIteration:
|
|
243
|
+
# get ring or linker atom
|
|
244
|
+
n, _ = min(bonds.items(), key=lambda x: len(x[1]))
|
|
245
|
+
m = bonds[n].pop()
|
|
246
|
+
bonds[m].discard(n)
|
|
247
|
+
|
|
248
|
+
for (nc, nr, nh), (i, (mc, mr, mh)) in product(atoms[n], enumerate(atoms[m])):
|
|
249
|
+
if nh == mh:
|
|
250
|
+
saturation.append((n, m, nh + 1))
|
|
251
|
+
electrons.append((n, nc, nr))
|
|
252
|
+
electrons.append((m, mc, mr))
|
|
253
|
+
|
|
254
|
+
for x in bonds.pop(n):
|
|
255
|
+
saturation.append((n, x, 1))
|
|
256
|
+
bonds[x].discard(n)
|
|
257
|
+
for x in bonds.pop(m):
|
|
258
|
+
saturation.append((m, x, 1))
|
|
259
|
+
bonds[x].discard(m)
|
|
260
|
+
break
|
|
261
|
+
elif nh < mh:
|
|
262
|
+
electrons.append((n, nc, nr))
|
|
263
|
+
saturation.append((n, m, nh + 1))
|
|
264
|
+
atoms[m].pop(i)
|
|
265
|
+
atoms[m].insert(i, (mc, mr, mh - nh))
|
|
266
|
+
|
|
267
|
+
for x in bonds.pop(n):
|
|
268
|
+
saturation.append((n, x, 1))
|
|
269
|
+
bonds[x].discard(n)
|
|
270
|
+
break
|
|
271
|
+
elif nh > mh:
|
|
272
|
+
electrons.append((m, mc, mr))
|
|
273
|
+
saturation.append((n, m, mh + 1))
|
|
274
|
+
atoms[n].pop(i)
|
|
275
|
+
atoms[n].insert(i, (nc, nr, nh - mh))
|
|
276
|
+
|
|
277
|
+
for x in bonds.pop(m):
|
|
278
|
+
saturation.append((m, x, 1))
|
|
279
|
+
bonds[x].discard(m)
|
|
280
|
+
break
|
|
281
|
+
else:
|
|
282
|
+
m = bonds.pop(n).pop()
|
|
283
|
+
bonds[m].discard(n)
|
|
284
|
+
|
|
285
|
+
for (nc, nr, nh), (i, (mc, mr, mh)) in product(atoms[n], enumerate(atoms[m])):
|
|
286
|
+
if nh == mh:
|
|
287
|
+
saturation.append((n, m, nh + 1))
|
|
288
|
+
electrons.append((n, nc, nr))
|
|
289
|
+
electrons.append((m, mc, mr))
|
|
290
|
+
for x in bonds.pop(m):
|
|
291
|
+
saturation.append((m, x, 1))
|
|
292
|
+
bonds[x].discard(m)
|
|
293
|
+
break
|
|
294
|
+
elif nh < mh and bonds[m]:
|
|
295
|
+
electrons.append((n, nc, nr))
|
|
296
|
+
saturation.append((n, m, nh + 1))
|
|
297
|
+
atoms[m].pop(i)
|
|
298
|
+
atoms[m].insert(i, (mc, mr, mh - nh))
|
|
299
|
+
break
|
|
300
|
+
else:
|
|
301
|
+
saturation.append((n, m, 1))
|
|
302
|
+
if not bonds[m]:
|
|
303
|
+
del bonds[m]
|
|
304
|
+
|
|
305
|
+
combo_ua = [] # possible single atoms electron states
|
|
306
|
+
for s in dots:
|
|
307
|
+
if len(s) == 1:
|
|
308
|
+
electrons.extend(s)
|
|
309
|
+
elif s:
|
|
310
|
+
combo_ua.append(s)
|
|
311
|
+
|
|
312
|
+
# if < 0 - we already in bad situation
|
|
313
|
+
# if > 0 - we need more radicals
|
|
314
|
+
need_radical = expected_radicals_count - sum(x for _, _, x in electrons)
|
|
315
|
+
need_charge = expected_charge - sum(x for _, x, _ in electrons)
|
|
316
|
+
if combo_ua:
|
|
317
|
+
# try randomly set charges and radicals.
|
|
318
|
+
# first pick required radical states.
|
|
319
|
+
# second try to minimize charge delta.
|
|
320
|
+
for attempt in range(1, len(combo_ua) + 1):
|
|
321
|
+
shuffle(combo_ua)
|
|
322
|
+
charges_radicals = []
|
|
323
|
+
rad = []
|
|
324
|
+
chg = []
|
|
325
|
+
for atom in combo_ua:
|
|
326
|
+
if len(rad) < need_radical: # pick radicals
|
|
327
|
+
r = next((x for x in atom if x[2]), None)
|
|
328
|
+
if r: # pick random radical states
|
|
329
|
+
rad.append(r)
|
|
330
|
+
else: # not radical
|
|
331
|
+
chg.append(atom)
|
|
332
|
+
else: # pick not radical states
|
|
333
|
+
c = [x for x in atom if not x[2]]
|
|
334
|
+
if len(c) > 1:
|
|
335
|
+
chg.append(c)
|
|
336
|
+
elif c:
|
|
337
|
+
charges_radicals.extend(c)
|
|
338
|
+
elif attempt == len(combo_ua): # all states has radical. balancing impossible
|
|
339
|
+
chg.append(atom) # fuck it horse. we in last attempt
|
|
340
|
+
else: # do next attempt
|
|
341
|
+
break
|
|
342
|
+
else:
|
|
343
|
+
charges_radicals.extend(rad)
|
|
344
|
+
current_charge = need_charge - sum(x for _, x, _ in charges_radicals)
|
|
345
|
+
current_radical = need_radical - len(rad)
|
|
346
|
+
for x in chg:
|
|
347
|
+
n, c, r = min(x, key=lambda x: abs(current_charge - x[1]))
|
|
348
|
+
charges_radicals.append((n, c, r))
|
|
349
|
+
current_charge -= c
|
|
350
|
+
current_radical -= r
|
|
351
|
+
|
|
352
|
+
if current_radical: # radical unbalanced
|
|
353
|
+
if current_charge:
|
|
354
|
+
log = 3
|
|
355
|
+
else:
|
|
356
|
+
log = 2
|
|
357
|
+
elif current_charge:
|
|
358
|
+
log = 1
|
|
359
|
+
else: # balanced!
|
|
360
|
+
log = 0
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
electrons.extend(charges_radicals)
|
|
364
|
+
elif need_radical:
|
|
365
|
+
log = 3 if need_charge else 2
|
|
366
|
+
elif need_charge:
|
|
367
|
+
log = 1
|
|
368
|
+
else:
|
|
369
|
+
log = 0
|
|
370
|
+
return saturation, electrons, log
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
__all__ = ['Saturation']
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2023-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# This file is part of chython.
|
|
5
|
+
#
|
|
6
|
+
# chython is free software; you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
8
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU Lesser General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
17
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
#
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from chython import smiles
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_canonicalize_nitro():
|
|
6
|
+
mol = smiles('c1ccccc1N(=O)=O')
|
|
7
|
+
mol.canonicalize()
|
|
8
|
+
s = str(mol)
|
|
9
|
+
assert '[N+]' in s and '[O-]' in s
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_canonicalize_benzene():
|
|
13
|
+
mol = smiles('C1=CC=CC=C1')
|
|
14
|
+
mol.canonicalize()
|
|
15
|
+
assert str(mol) == 'c1ccccc1'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_kekule_thiele():
|
|
19
|
+
mol = smiles('c1ccccc1')
|
|
20
|
+
mol.kekule()
|
|
21
|
+
s = str(mol)
|
|
22
|
+
assert 'c' not in s
|
|
23
|
+
assert '=' in s
|
|
24
|
+
mol.thiele()
|
|
25
|
+
assert str(mol) == 'c1ccccc1'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_standardize():
|
|
29
|
+
mol = smiles('c1ccccc1N(=O)=O')
|
|
30
|
+
mol.standardize()
|
|
31
|
+
s = str(mol)
|
|
32
|
+
assert '[N+]' in s
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_neutralize():
|
|
36
|
+
mol = smiles('[NH3+]CC(=O)[O-]')
|
|
37
|
+
mol.neutralize()
|
|
38
|
+
s = str(mol)
|
|
39
|
+
assert '+' not in s or '-' not in s
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_check_valence_clean():
|
|
43
|
+
mol = smiles('CCO')
|
|
44
|
+
errors = mol.check_valence()
|
|
45
|
+
assert errors == []
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_canonicalize_idempotent():
|
|
49
|
+
mol = smiles('OCC')
|
|
50
|
+
mol.canonicalize()
|
|
51
|
+
s1 = str(mol)
|
|
52
|
+
mol.canonicalize()
|
|
53
|
+
s2 = str(mol)
|
|
54
|
+
assert s1 == s2
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_keep_kekule_after_charge_isomer_standardization():
|
|
58
|
+
mol = smiles('C=1N(C)C=[N+](CC)C=1')
|
|
59
|
+
mol.canonicalize(keep_kekule=True)
|
|
60
|
+
|
|
61
|
+
aromatic = mol.copy()
|
|
62
|
+
aromatic.thiele()
|
|
63
|
+
|
|
64
|
+
expected = smiles('c1[n+](C)ccn1CC')
|
|
65
|
+
expected.canonicalize()
|
|
66
|
+
assert aromatic == expected
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2023-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# This file is part of chython.
|
|
5
|
+
#
|
|
6
|
+
# chython is free software; you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
8
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU Lesser General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
17
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
#
|
|
19
|
+
from chython import smiles
|
|
20
|
+
from pytest import mark
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
data = [
|
|
24
|
+
('CP(C)(C)C', 'C[P+](C)(C)C'),
|
|
25
|
+
('CB1(C)[H]B(C)(C)[H]1', 'CB1(C)~[H]B(C)(C)~[H]1'),
|
|
26
|
+
('[O]N(C)[NH]', '[O-][N+](C)=N'), ('[O]N(C)[CH2]', '[O-][N+](C)=C'),
|
|
27
|
+
('[O]S(C)(C)[O]', 'O=S(C)(C)=O'), ('[O]S(C)(C)[S]', 'O=S(C)(C)=S'),
|
|
28
|
+
('BN(C)=C', 'B~N(C)=C'),
|
|
29
|
+
('B=N(C)(C)C', 'B~N(C)(C)C'),
|
|
30
|
+
('BS(C)C', 'B~S(C)C'), ('BO(C)C', 'B~O(C)C'),
|
|
31
|
+
('[B-]=[N+](C)C', 'BN(C)C'), ('C[B-]=[N+]C', 'CBNC'), ('[B-]=[N+]', 'BN'),
|
|
32
|
+
('[O-][B+3]([O-])([O-])[O-]', 'O[B-](O)(O)O'),
|
|
33
|
+
('[O-]B(O)(O)O', 'O[B-](O)(O)O'),
|
|
34
|
+
('OB(O)(O)O', 'O[B-](O)(O)O'),
|
|
35
|
+
('CN(C)(C)C', 'C[N+](C)(C)C'),
|
|
36
|
+
('C=N(=O)O', 'C[N+](=O)[O-]'),
|
|
37
|
+
('C=N(=O)C', 'C=[N+]([O-])C'), ('O=N(=O)C', 'O=[N+]([O-])C'), ('N=N(=O)C', 'N=[N+]([O-])C'),
|
|
38
|
+
('C=[N+]([O-])O', 'C[N+](=O)[O-]'),
|
|
39
|
+
('CN(=O)=N(=O)C', 'C[N+]([O-])=[N+]([O-])C'),
|
|
40
|
+
('CN(=O)=N(=N)C', 'C[N+]([O-])=[N+]([NH-])C'), ('CN(=O)=N(=NC)C', 'C[N+]([O-])=[N+]([N-]C)C'),
|
|
41
|
+
('C=N(=N)C', 'C=[N+]([N-])C'), ('C=N(=NC)C', 'C=[N+]([N-]C)C'), ('N=N(=N)C', 'N=[N+]([N-])C'),
|
|
42
|
+
('[N-][N+](=O)C', 'N=[N+]([O-])C'), ('C[N-][N+](=O)C', 'CN=[N+]([O-])C'),
|
|
43
|
+
('[O-]N(=O)=O', '[O-][N+](=O)[O-]'),
|
|
44
|
+
('CN(:O):O', 'C[N+](=O)[O-]'),
|
|
45
|
+
('O=[N-]=O', '[O-]N=O'),
|
|
46
|
+
('O=N#N', 'O=[N+]=[N-]'), ('C=N#N', 'C=[N+]=[N-]'), ('N=N#N', 'N=[N+]=[N-]'),
|
|
47
|
+
('[O-][N+]#N', 'O=[N+]=[N-]'), ('C[CH-][N+]#N', 'CC=[N+]=[N-]'), ('[NH-][N+]#N', 'N=[N+]=[N-]'),
|
|
48
|
+
('C[N+]#N=[N-]', 'CN=[N+]=[N-]'),
|
|
49
|
+
('CN=N=N', 'CN=[N+]=[N-]'),
|
|
50
|
+
('CNN#N', 'CN=[N+]=[N-]'),
|
|
51
|
+
('[N-]#N=NC', '[N-]=[N+]=NC'),
|
|
52
|
+
('[N-]=N#N', '[N-]=[N+]=[N-]'),
|
|
53
|
+
('CC#N=N', 'CC=[N+]=[N-]'),
|
|
54
|
+
('CC#N=NC', 'CC#[N+][N-]C'), ('CC#N=O', 'CC#[N+][O-]'),
|
|
55
|
+
('NN#C', '[NH-][N+]#C'), ('ON#CC', '[O-][N+]#CC'), ('SN#CC', '[S-][N+]#CC'),
|
|
56
|
+
('CNN#C', 'C[N-][N+]#C'),
|
|
57
|
+
('N[N+]#[C-]', '[NH-][N+]#C'), ('O[N+]#[C-]', '[O-][N+]#C'), ('S[N+]#[C-]', '[S-][N+]#C'),
|
|
58
|
+
('CN[N+]#[C-]', 'C[N-][N+]#C'),
|
|
59
|
+
('CN#C', 'C[N+]#[C-]'),
|
|
60
|
+
('C[C-]=[N+]=N', 'CC=[N+]=[N-]'),
|
|
61
|
+
('CN(C)(C)=O', 'C[N+](C)(C)[O-]'), ('CN(C)(C)=NC', 'C[N+](C)(C)[N-]C'),
|
|
62
|
+
('C[N](C)=O |^1:1|', 'CN(C)[O] |^1:3|'),
|
|
63
|
+
('C=N(C)[O] |^1:3|', 'C=[N+](C)[O-]'),
|
|
64
|
+
('CN(C)#N', 'C[N+](C)=[N-]'),
|
|
65
|
+
('CN=[N+]', 'C[N+]#N'),
|
|
66
|
+
('[NH2+][C-]=O', 'N=C=O'), ('C[NH+][C-]=O', 'CN=C=O'),
|
|
67
|
+
('N#CO', 'N=C=O'),
|
|
68
|
+
('N#C[O-]', '[N-]=C=O'),
|
|
69
|
+
('CC(C)(C)[N+][O-]', 'CC(C)(C)N=O'),
|
|
70
|
+
('CN=O', 'C=NO'),
|
|
71
|
+
('NC=[O+]C', '[NH2+]=COC'), ('CNC=[O+]C', 'C[NH+]=COC'), ('CN(C)C=[O+]C', 'C[N+](C)=COC'),
|
|
72
|
+
# amide rule: N=C-OH >> NH-C=O
|
|
73
|
+
('N=CO', 'NC=O'), ('N=CS', 'NC=S'),
|
|
74
|
+
# ring amidation (6-membered): OH-C=N in ring >> O=C-NH in ring
|
|
75
|
+
('OC1=CC=CC=N1', 'O=C1NC=CC=C1'),
|
|
76
|
+
('OC1=CC=NC=N1', 'O=C1NC=NC=C1'),
|
|
77
|
+
('OC1=NC=CC=N1', 'O=C1N=CC=CN1'),
|
|
78
|
+
('OC1=C(O)N=CC=N1', 'O=C1NC=CNC1=O'),
|
|
79
|
+
('OC1=NC=CN=C1O', 'O=C1NC=CNC1=O'),
|
|
80
|
+
('OC1=CC=NC(=O)N1', 'O=C1NC=CC(=O)N1'),
|
|
81
|
+
('OC1=CC=NC(O)=N1', 'O=C1NC=CC(=O)N1'),
|
|
82
|
+
('OC1=NC(O)=NC=C1', 'O=C1NC=CC(=O)N1'),
|
|
83
|
+
# 5-membered ring amidation (short flip)
|
|
84
|
+
('CN1C=CC(O)=N1', 'CN1NC(=O)C=C1'),
|
|
85
|
+
# 5-membered ring amidation (long flip)
|
|
86
|
+
('CN1N=CC=C1O', 'CN1NC=CC1=O'),
|
|
87
|
+
# hydroxypyridine to pyridone
|
|
88
|
+
('OC1=CC=NC=C1', 'O=C1C=CNC=C1'),
|
|
89
|
+
# 6-membered ring N=C-CH adjacent to C=O (short flip)
|
|
90
|
+
('O=C1C=CCC=N1', 'O=C1NC=CC=C1'),
|
|
91
|
+
# 6-membered ring N=C-C=C-CH adjacent to C=O (long flip)
|
|
92
|
+
('O=C1CC=CC=N1', 'O=C1NC=CC=C1'),
|
|
93
|
+
# 6-membered ring N=C-CH adjacent to C=O (C=O between CH and ring end)
|
|
94
|
+
('O=C1CC=NC=C1', 'O=C1C=CNC=C1'),
|
|
95
|
+
# 5-membered ring N=C-CH >> NH-C=C
|
|
96
|
+
('C1C=NC=N1', 'N1C=CN=C1'),
|
|
97
|
+
# 6-membered ring exocyclic C=N to C-NH (cytosine-like)
|
|
98
|
+
('N=C1NC=CC(=O)N1', 'NC1=NC=CC(=O)N1'),
|
|
99
|
+
# 5-membered ring N=C-CH with sp3 N,O closure
|
|
100
|
+
('CN1N=CCC1=O', 'CN1NC=CC1=O'),
|
|
101
|
+
# acyclic enol (51)
|
|
102
|
+
('OC=C', 'O=CC'), ('OC(C)=C', 'O=C(C)C'),
|
|
103
|
+
# P rules
|
|
104
|
+
('[O-][P+](C)(C)C', 'O=P(C)(C)C'),
|
|
105
|
+
('[CH2+][P-](C)(C)C', 'C=P(C)(C)C'),
|
|
106
|
+
('FP(F)(F)(F)(F)F', 'F[P-](F)(F)(F)(F)F'),
|
|
107
|
+
('C[P-](C)(=O)=O', 'CP(C)(=O)[O-]'),
|
|
108
|
+
('CP(C)(=O)[S-]', 'CP(C)(=S)[O-]'),
|
|
109
|
+
('CP(C)(=O)S', 'CP(C)(=S)O'),
|
|
110
|
+
# S rules
|
|
111
|
+
('CS(=O)(=O)[S-]', 'CS(=O)(=S)[O-]'),
|
|
112
|
+
('CS(=O)(=O)S', 'CS(=O)(=S)O'),
|
|
113
|
+
('C[S+](C)[O-]', 'CS(C)=O'),
|
|
114
|
+
('O=[S+][O-]', 'O=S=O'), ('O=[S+](C)(C)[O-]', 'O=S(C)(C)=O'),
|
|
115
|
+
('O=[S+2]([O-])[O-]', 'O=S(=O)=O'),
|
|
116
|
+
('C[S+2](C)([O-])[O-]', 'CS(C)(=O)=O'),
|
|
117
|
+
('C[S-](C)[CH2+]', 'CS(C)=C'),
|
|
118
|
+
('O=[S-](C)=O', 'O=S(C)[O-]'),
|
|
119
|
+
('O=[S-](C)(=O)=O', 'O=S(C)(=O)[O-]'),
|
|
120
|
+
('O=[S-](=O)[S-]', 'S=S([O-])[O-]'),
|
|
121
|
+
('CS(C)(=O)O', 'CS(C)(=O)=O'),
|
|
122
|
+
('CS(C)(=O)N', 'CS(C)(=O)=N'),
|
|
123
|
+
('N=S(C)O', 'NS(C)=O'), ('N=S(C)(C)(C)O', 'NS(C)(C)(C)=O'),
|
|
124
|
+
# C rules
|
|
125
|
+
('C#CO', 'C=C=O'),
|
|
126
|
+
('C#CNC', 'C=C=NC'),
|
|
127
|
+
('C=O |^1:0|', '[C-]#[O+]'),
|
|
128
|
+
('[O]O[O] |^1:0,2|', 'O=[O+][O-]'),
|
|
129
|
+
('[CH2+]N(C)C', 'C=[N+](C)C'),
|
|
130
|
+
('[CH2+]N(C)O', 'C=[N+](C)O'),
|
|
131
|
+
('[CH2+]=NC', 'C#[N+]C'),
|
|
132
|
+
# Cl rules
|
|
133
|
+
('O[Cl+][O-]', 'OCl=O'),
|
|
134
|
+
('O[Cl+2]([O-])[O-]', 'OCl(=O)=O'),
|
|
135
|
+
('O[Cl+3]([O-])([O-])[O-]', 'OCl(=O)(=O)=O'),
|
|
136
|
+
('[Cl-]=O', 'Cl[O-]'),
|
|
137
|
+
# S=N double rules
|
|
138
|
+
('OS(=N)(=N)O', 'O=S(N)(N)=O'), ('OS(=N)(=N)C', 'O=S(N)(=N)C')
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@mark.parametrize('raw,result', data)
|
|
143
|
+
def test_group(raw, result):
|
|
144
|
+
tmp = smiles(raw)
|
|
145
|
+
tmp.standardize()
|
|
146
|
+
assert tmp == smiles(result), f'{raw} > {tmp} != {result}'
|