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,597 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2017-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2019 Timur Gimadiev <timur.gimadiev@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 abc import ABC, abstractmethod
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from functools import cached_property
|
|
23
|
+
from heapq import heappop, heappush
|
|
24
|
+
from itertools import product
|
|
25
|
+
from random import random
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
from .._functions import cached_method
|
|
28
|
+
|
|
29
|
+
charge_str = {-4: '-4', -3: '-3', -2: '-2', -1: '-', 0: '0', 1: '+', 2: '+2', 3: '+3', 4: '+4'}
|
|
30
|
+
order_str = {1: '-', 2: '=', 3: '#', 4: ':', 8: '~', None: '.'}
|
|
31
|
+
organic_set = {'C', 'N', 'O', 'P', 'S', 'F', 'Cl', 'Br', 'I', 'B'}
|
|
32
|
+
dyn_order_str = {(None, 1): '[.>-]', (None, 2): '[.>=]', (None, 3): '[.>#]', (None, 4): '[.>:]', (None, 8): '[.>~]',
|
|
33
|
+
(1, None): '[->.]', (1, 1): '', (1, 2): '[->=]', (1, 3): '[->#]', (1, 4): '[->:]', (1, 8): '[->~]',
|
|
34
|
+
(2, None): '[=>.]', (2, 1): '[=>-]', (2, 2): '=', (2, 3): '[=>#]', (2, 4): '[=>:]', (2, 8): '[=>~]',
|
|
35
|
+
(3, None): '[#>.]', (3, 1): '[#>-]', (3, 2): '[#>=]', (3, 3): '#', (3, 4): '[#>:]', (3, 8): '[#>~]',
|
|
36
|
+
(4, None): '[:>.]', (4, 1): '[:>-]', (4, 2): '[:>=]', (4, 3): '[:>#]', (4, 4): ':', (4, 8): '[:>~]',
|
|
37
|
+
(8, None): '[~>.]', (8, 1): '[~>-]', (8, 2): '[~>=]', (8, 3): '[~>#]', (8, 4): '[~>:]', (8, 8): '~'}
|
|
38
|
+
|
|
39
|
+
dyn_charge_str = {(i, j): f'{charge_str[i]}>{charge_str[j]}' if i != j else charge_str[i]
|
|
40
|
+
for i, j in product(range(-4, 5), repeat=2)}
|
|
41
|
+
dyn_charge_str[(0, 0)] = ''
|
|
42
|
+
|
|
43
|
+
dyn_radical_str = {(True, True): '*', (True, False): '*>^', (False, True): '^>*'}
|
|
44
|
+
|
|
45
|
+
# atomic number constants
|
|
46
|
+
B = 5
|
|
47
|
+
C = 6
|
|
48
|
+
N = 7
|
|
49
|
+
P = 15
|
|
50
|
+
S = 16
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Smiles(ABC):
|
|
54
|
+
__slots__ = ()
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def smiles(self) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Generate SMILES string of the molecule.
|
|
60
|
+
"""
|
|
61
|
+
return str(self)
|
|
62
|
+
|
|
63
|
+
@cached_method
|
|
64
|
+
def __str__(self):
|
|
65
|
+
smiles, order = self._smiles(self._smiles_order(), _return_order=True)
|
|
66
|
+
if (cx := self._format_cxsmiles(order)) is not None:
|
|
67
|
+
smiles.append(' ')
|
|
68
|
+
smiles.append(cx)
|
|
69
|
+
|
|
70
|
+
self.__dict__['smiles_atoms_order'] = tuple(order) # cache smiles_atoms_order
|
|
71
|
+
return ''.join(smiles)
|
|
72
|
+
|
|
73
|
+
def __format__(self, format_spec, *, _return_order=False):
|
|
74
|
+
"""
|
|
75
|
+
Signature generation options.
|
|
76
|
+
|
|
77
|
+
:param format_spec: String with keys:
|
|
78
|
+
a - Generate asymmetric closures.
|
|
79
|
+
!s - Disable stereo marks.
|
|
80
|
+
A - Use aromatic bonds instead aromatic atoms.
|
|
81
|
+
m - Set atom mapping.
|
|
82
|
+
r - Generate random-ordered smiles.
|
|
83
|
+
h - Show implicit hydrogens.
|
|
84
|
+
!b - Disable bonds tokens.
|
|
85
|
+
!x - Disable CXSMILES extension.
|
|
86
|
+
!z - Disable charge representation.
|
|
87
|
+
|
|
88
|
+
Combining possible. Order independent. Another keys ignored.
|
|
89
|
+
"""
|
|
90
|
+
if format_spec:
|
|
91
|
+
kwargs = {}
|
|
92
|
+
if 'a' in format_spec:
|
|
93
|
+
kwargs['asymmetric_closures'] = True
|
|
94
|
+
if '!s' in format_spec:
|
|
95
|
+
kwargs['stereo'] = False
|
|
96
|
+
if 'A' in format_spec:
|
|
97
|
+
kwargs['aromatic'] = False
|
|
98
|
+
if 'm' in format_spec:
|
|
99
|
+
kwargs['mapping'] = True
|
|
100
|
+
if 'h' in format_spec:
|
|
101
|
+
kwargs['hydrogens'] = True
|
|
102
|
+
if '!b' in format_spec:
|
|
103
|
+
kwargs['bonds'] = False
|
|
104
|
+
if '!z' in format_spec:
|
|
105
|
+
kwargs['charges'] = False
|
|
106
|
+
|
|
107
|
+
if 'r' in format_spec:
|
|
108
|
+
kwargs['random'] = True
|
|
109
|
+
|
|
110
|
+
def w(_):
|
|
111
|
+
return random()
|
|
112
|
+
else:
|
|
113
|
+
w = self._smiles_order('!s' not in format_spec)
|
|
114
|
+
|
|
115
|
+
smiles, order = self._smiles(w, _return_order=True, **kwargs)
|
|
116
|
+
if _return_order:
|
|
117
|
+
return ''.join(smiles), order
|
|
118
|
+
elif '!x' in format_spec or (cx := self._format_cxsmiles(order)) is None:
|
|
119
|
+
return ''.join(smiles)
|
|
120
|
+
else:
|
|
121
|
+
smiles.append(' ')
|
|
122
|
+
smiles.append(cx)
|
|
123
|
+
return ''.join(smiles)
|
|
124
|
+
|
|
125
|
+
elif _return_order:
|
|
126
|
+
smiles, order = self._smiles(self._smiles_order(), _return_order=True)
|
|
127
|
+
smiles = ''.join(smiles)
|
|
128
|
+
if (cx := self._format_cxsmiles(order)) is not None: # cache molecule smiles
|
|
129
|
+
self.__dict__['__cached_method___str__'] = f'{smiles} {cx}'
|
|
130
|
+
else:
|
|
131
|
+
self.__dict__['__cached_method___str__'] = smiles
|
|
132
|
+
self.__dict__['smiles_atoms_order'] = tuple(order) # cache smiles_atoms_order
|
|
133
|
+
return smiles, order
|
|
134
|
+
return str(self)
|
|
135
|
+
|
|
136
|
+
def __eq__(self, other):
|
|
137
|
+
return isinstance(other, Smiles) and str(self) == str(other)
|
|
138
|
+
|
|
139
|
+
@cached_method
|
|
140
|
+
def __hash__(self):
|
|
141
|
+
return hash(str(self))
|
|
142
|
+
|
|
143
|
+
@cached_property
|
|
144
|
+
def smiles_atoms_order(self) -> tuple[int, ...]:
|
|
145
|
+
"""
|
|
146
|
+
Atoms order in canonic SMILES.
|
|
147
|
+
"""
|
|
148
|
+
smiles, order = self._smiles(self._smiles_order(), _return_order=True)
|
|
149
|
+
if (cx := self._format_cxsmiles(order)) is not None:
|
|
150
|
+
smiles.append(' ')
|
|
151
|
+
smiles.append(cx)
|
|
152
|
+
self.__dict__['__cached_method___str__'] = ''.join(smiles) # cache smiles
|
|
153
|
+
return tuple(order)
|
|
154
|
+
|
|
155
|
+
def _smiles(self, weights, *, asymmetric_closures=False,
|
|
156
|
+
open_parenthesis='(', close_parenthesis=')', delimiter='.', _return_order=False, **kwargs):
|
|
157
|
+
if not self._atoms:
|
|
158
|
+
return []
|
|
159
|
+
bonds = self._bonds
|
|
160
|
+
atoms_set = set(self._atoms)
|
|
161
|
+
seen = {}
|
|
162
|
+
cycle = 0
|
|
163
|
+
casted_cycles = {}
|
|
164
|
+
string = []
|
|
165
|
+
order = []
|
|
166
|
+
visited_bond = set()
|
|
167
|
+
heap = list(range(1, 100))
|
|
168
|
+
|
|
169
|
+
if kwargs.get('random', False):
|
|
170
|
+
mod_weights_start = mod_weights = weights
|
|
171
|
+
else:
|
|
172
|
+
groups = defaultdict(int)
|
|
173
|
+
for n in atoms_set:
|
|
174
|
+
groups[weights(n)] -= 1
|
|
175
|
+
|
|
176
|
+
def mod_weights_start(x):
|
|
177
|
+
return (groups[weights(x)], # common groups
|
|
178
|
+
weights(x)) # smallest weight
|
|
179
|
+
|
|
180
|
+
def mod_weights(x):
|
|
181
|
+
return (groups[weights(x)], # common groups
|
|
182
|
+
weights(x), # smallest weight
|
|
183
|
+
seen[x]) # BFS nearest to starting
|
|
184
|
+
|
|
185
|
+
while True:
|
|
186
|
+
start = min(atoms_set, key=mod_weights_start)
|
|
187
|
+
if not kwargs.get('random', False):
|
|
188
|
+
seen[start] = 0
|
|
189
|
+
queue = [(start, 1)]
|
|
190
|
+
while queue:
|
|
191
|
+
n, d = queue.pop(0)
|
|
192
|
+
for m in bonds[n].keys() - seen.keys():
|
|
193
|
+
queue.append((m, d + 1))
|
|
194
|
+
seen[m] = d
|
|
195
|
+
|
|
196
|
+
# modified NX dfs with cycle detection
|
|
197
|
+
stack = [(start, len(atoms_set), iter(sorted(bonds[start], key=mod_weights)))]
|
|
198
|
+
visited = {start: []} # predecessors for stereo. atom: (visited[atom], *edges[atom])
|
|
199
|
+
disconnected = set()
|
|
200
|
+
edges = defaultdict(list)
|
|
201
|
+
tokens = defaultdict(list)
|
|
202
|
+
while stack:
|
|
203
|
+
parent, depth_now, children = stack[-1]
|
|
204
|
+
try:
|
|
205
|
+
child = next(children)
|
|
206
|
+
except StopIteration:
|
|
207
|
+
stack.pop()
|
|
208
|
+
else:
|
|
209
|
+
if child not in visited:
|
|
210
|
+
edges[parent].append(child)
|
|
211
|
+
visited[child] = [parent]
|
|
212
|
+
if depth_now > 1:
|
|
213
|
+
front = bonds[child].keys() - {parent}
|
|
214
|
+
if front:
|
|
215
|
+
stack.append((child, depth_now - 1, iter(sorted(front, key=mod_weights))))
|
|
216
|
+
elif (child, parent) not in disconnected:
|
|
217
|
+
disconnected.add((parent, child))
|
|
218
|
+
disconnected.add((child, parent))
|
|
219
|
+
cycle += 1
|
|
220
|
+
tokens[parent].append((child, cycle))
|
|
221
|
+
tokens[child].append((parent, cycle))
|
|
222
|
+
|
|
223
|
+
# flatten directed graph: edges
|
|
224
|
+
stack = [[start, 0, [start]]]
|
|
225
|
+
while True:
|
|
226
|
+
tail, closure, smiles = stack[-1]
|
|
227
|
+
if tail in edges:
|
|
228
|
+
children = edges[tail]
|
|
229
|
+
if len(children) > 1: # has side chain
|
|
230
|
+
child = children[-1]
|
|
231
|
+
stack_len = len(stack)
|
|
232
|
+
stack.append([child, 0, [(tail, child), child]]) # end of current chain
|
|
233
|
+
for child in children[-2::-1]: # start side chains
|
|
234
|
+
stack.append([child, stack_len, ['(', (tail, child), child]])
|
|
235
|
+
else: # chain grow
|
|
236
|
+
child = children[-1]
|
|
237
|
+
stack[-1][0] = child
|
|
238
|
+
smiles.append((tail, child))
|
|
239
|
+
smiles.append(child)
|
|
240
|
+
elif closure: # end of side chain
|
|
241
|
+
stack.pop()
|
|
242
|
+
if smiles[-2] == '(':
|
|
243
|
+
smiles.pop(-2)
|
|
244
|
+
else:
|
|
245
|
+
smiles.append(')')
|
|
246
|
+
stack[closure - 1][2].extend(smiles)
|
|
247
|
+
elif len(stack) > 2:
|
|
248
|
+
stack.pop()
|
|
249
|
+
stack[-1][0] = tail
|
|
250
|
+
stack[-1][2].extend(smiles)
|
|
251
|
+
elif len(stack) == 2:
|
|
252
|
+
stack[0][2].extend(smiles)
|
|
253
|
+
smiles = stack[0][2]
|
|
254
|
+
break
|
|
255
|
+
else:
|
|
256
|
+
break
|
|
257
|
+
|
|
258
|
+
# get order of each atom in ring closures
|
|
259
|
+
rings_order = {token: n for n, token in enumerate(smiles) if token in tokens}
|
|
260
|
+
for token in rings_order: # prepare closure numbers
|
|
261
|
+
released = []
|
|
262
|
+
# order closure atoms as in smiles string
|
|
263
|
+
for _, c in sorted(tokens[token], key=lambda x: rings_order[x[0]]):
|
|
264
|
+
if c in casted_cycles: # release ring closure number
|
|
265
|
+
released.append(casted_cycles[c])
|
|
266
|
+
else:
|
|
267
|
+
casted_cycles[c] = heappop(heap)
|
|
268
|
+
for c in released: # delayed release to avoid duplicates. e.g. C1..C11..C1 instead of C1..C12..C2
|
|
269
|
+
heappush(heap, c)
|
|
270
|
+
|
|
271
|
+
# prepare new neighbors order for stereo sign calculation
|
|
272
|
+
for token in smiles:
|
|
273
|
+
if token in tokens:
|
|
274
|
+
tokens[token].sort(key=lambda x: casted_cycles[x[1]]) # order closures
|
|
275
|
+
visited[token].extend(n for n, _ in tokens[token])
|
|
276
|
+
if token in edges:
|
|
277
|
+
visited[token].extend(edges[token])
|
|
278
|
+
|
|
279
|
+
for token in smiles:
|
|
280
|
+
if isinstance(token, int): # atoms
|
|
281
|
+
string.append(self._format_atom(token, visited, **kwargs))
|
|
282
|
+
order.append(token)
|
|
283
|
+
if token in tokens:
|
|
284
|
+
for m, c in tokens[token]:
|
|
285
|
+
if asymmetric_closures:
|
|
286
|
+
if (token, m) not in visited_bond:
|
|
287
|
+
string.append(self._format_bond(token, m, visited, **kwargs))
|
|
288
|
+
visited_bond.add((m, token))
|
|
289
|
+
else:
|
|
290
|
+
string.append(self._format_bond(token, m, visited, **kwargs))
|
|
291
|
+
string.append(self._format_closure(casted_cycles[c]))
|
|
292
|
+
elif token == '(':
|
|
293
|
+
string.append(open_parenthesis)
|
|
294
|
+
elif token == ')':
|
|
295
|
+
string.append(close_parenthesis)
|
|
296
|
+
else: # bonds
|
|
297
|
+
string.append(self._format_bond(*token, visited, **kwargs))
|
|
298
|
+
|
|
299
|
+
atoms_set.difference_update(visited)
|
|
300
|
+
if atoms_set:
|
|
301
|
+
string.append(delimiter)
|
|
302
|
+
else:
|
|
303
|
+
break
|
|
304
|
+
if _return_order:
|
|
305
|
+
return string, order
|
|
306
|
+
return string
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def _format_closure(c):
|
|
310
|
+
return str(c) if c < 10 else f'%{c}'
|
|
311
|
+
|
|
312
|
+
@abstractmethod
|
|
313
|
+
def _format_atom(self, n, adjacency, **kwargs):
|
|
314
|
+
...
|
|
315
|
+
|
|
316
|
+
@abstractmethod
|
|
317
|
+
def _format_bond(self, n, m, adjacency, **kwargs):
|
|
318
|
+
...
|
|
319
|
+
|
|
320
|
+
@abstractmethod
|
|
321
|
+
def _smiles_order(self, stereo=True) -> Callable:
|
|
322
|
+
...
|
|
323
|
+
|
|
324
|
+
def _format_cxsmiles(self, order) -> str | None:
|
|
325
|
+
...
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class MoleculeSmiles(Smiles):
|
|
329
|
+
__slots__ = ()
|
|
330
|
+
|
|
331
|
+
def sticky_smiles(self, left: int = None, right: int = None, *,
|
|
332
|
+
remove_left: bool = False, remove_right: bool = False, tries: int = 10,
|
|
333
|
+
keep_bond_left: bool = False, keep_bond_right: bool = False, hydrogens: bool = False) -> str:
|
|
334
|
+
"""
|
|
335
|
+
Generate smiles with fixed left and/or right terminal atoms.
|
|
336
|
+
The right atom must be terminal if set. Use a temporary attached atom with remove_right=True as a workaround.
|
|
337
|
+
Removing in-ring terminal atoms can lead to invalid smiles.
|
|
338
|
+
|
|
339
|
+
:param remove_left: drop terminal atom and corresponding bond by default
|
|
340
|
+
:param remove_right: drop terminal atom and corresponding bond by default
|
|
341
|
+
:param tries: number of attempts to generate smiles
|
|
342
|
+
:param keep_bond_left: keep bond of removed left atom. Sets explicit single bond
|
|
343
|
+
:param keep_bond_right: keep bond of removed right atom
|
|
344
|
+
:param hydrogens: show implicit hydrogens in smiles
|
|
345
|
+
"""
|
|
346
|
+
bonds = self._bonds
|
|
347
|
+
if right:
|
|
348
|
+
assert tries > 0, 'tries count should be positive'
|
|
349
|
+
assert len(bonds[right]) == 1, 'right atom should be terminal'
|
|
350
|
+
assert left != right, 'left and right atoms the same'
|
|
351
|
+
assert self.connected_components_count == 1, 'only single component structures supported'
|
|
352
|
+
if remove_left: assert left, 'specify left atom to remove'
|
|
353
|
+
|
|
354
|
+
seen = {right: 0}
|
|
355
|
+
queue = [(right, -10)]
|
|
356
|
+
while queue:
|
|
357
|
+
n, d = queue.pop(0)
|
|
358
|
+
for m in bonds[n].keys() - seen.keys():
|
|
359
|
+
queue.append((m, d - 10))
|
|
360
|
+
seen[m] = d
|
|
361
|
+
if left:
|
|
362
|
+
bonds[left] # noqa. check left atom availability
|
|
363
|
+
seen[left] = -1_000_000_000 # prioritize left atom
|
|
364
|
+
|
|
365
|
+
for _ in range(tries):
|
|
366
|
+
smiles, order = self._smiles(lambda x: seen[x] + random(), _return_order=True,
|
|
367
|
+
random=True, hydrogens=hydrogens)
|
|
368
|
+
if order[-1] == right:
|
|
369
|
+
break
|
|
370
|
+
else:
|
|
371
|
+
raise Exception('generation of smiles failed')
|
|
372
|
+
if remove_left:
|
|
373
|
+
if keep_bond_left:
|
|
374
|
+
smiles = smiles[1:]
|
|
375
|
+
if not smiles[0]:
|
|
376
|
+
smiles[0] = '-'
|
|
377
|
+
else:
|
|
378
|
+
smiles = smiles[2:]
|
|
379
|
+
if remove_right:
|
|
380
|
+
if keep_bond_right:
|
|
381
|
+
smiles = smiles[:-1]
|
|
382
|
+
if not smiles[-1]:
|
|
383
|
+
smiles[-1] = '-'
|
|
384
|
+
else:
|
|
385
|
+
smiles = smiles[:-2]
|
|
386
|
+
elif left:
|
|
387
|
+
bonds[left] # noqa. check left atom availability
|
|
388
|
+
smiles = self._smiles(lambda x: x != left, random=True, hydrogens=hydrogens)
|
|
389
|
+
if remove_left:
|
|
390
|
+
if keep_bond_left:
|
|
391
|
+
smiles = smiles[1:]
|
|
392
|
+
if not smiles[0]:
|
|
393
|
+
smiles[0] = '-' # assuming nobody will try to remove atom from an aromatic ring
|
|
394
|
+
else:
|
|
395
|
+
smiles = smiles[2:]
|
|
396
|
+
else:
|
|
397
|
+
raise ValueError('either left or right atom should be specified')
|
|
398
|
+
return ''.join(smiles)
|
|
399
|
+
|
|
400
|
+
def _smiles_order(self, stereo=True):
|
|
401
|
+
if stereo:
|
|
402
|
+
return self._chiral_morgan.__getitem__
|
|
403
|
+
else:
|
|
404
|
+
return self.atoms_order.__getitem__
|
|
405
|
+
|
|
406
|
+
def _format_cxsmiles(self, order):
|
|
407
|
+
cx = []
|
|
408
|
+
rd = []
|
|
409
|
+
es = defaultdict(list)
|
|
410
|
+
|
|
411
|
+
atoms = self._atoms
|
|
412
|
+
for i, n in enumerate(order):
|
|
413
|
+
a = atoms[n]
|
|
414
|
+
if a.is_radical:
|
|
415
|
+
rd.append(str(i))
|
|
416
|
+
if (s := a.extended_stereo) is not None:
|
|
417
|
+
if s < 0:
|
|
418
|
+
es[f'o{-s}:'].append(str(i))
|
|
419
|
+
elif s > 0:
|
|
420
|
+
es[f'&{s}:'].append(str(i))
|
|
421
|
+
|
|
422
|
+
if rd:
|
|
423
|
+
cx.append('^1:' + ','.join(rd))
|
|
424
|
+
if es:
|
|
425
|
+
for k in sorted(es):
|
|
426
|
+
cx.append(k + ','.join(es[k]))
|
|
427
|
+
if cx:
|
|
428
|
+
return '|' + ','.join(cx) + '|'
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
def _format_atom(self, n, adjacency, **kwargs):
|
|
432
|
+
atom = self._atoms[n]
|
|
433
|
+
|
|
434
|
+
smi = ['', # [
|
|
435
|
+
str(atom.isotope) if atom.isotope else '', # isotope
|
|
436
|
+
None,
|
|
437
|
+
'', # stereo
|
|
438
|
+
'', # hydrogen
|
|
439
|
+
'', # charge
|
|
440
|
+
f':{n}' if kwargs.get('mapping', False) else '', # mapping
|
|
441
|
+
''] # ]
|
|
442
|
+
|
|
443
|
+
if atom.stereo is not None and kwargs.get('stereo', True):
|
|
444
|
+
# allene
|
|
445
|
+
if n in self._stereo_allenes_terminals:
|
|
446
|
+
t1, t2 = self._stereo_allenes_terminals[n]
|
|
447
|
+
env = self.stereogenic_allenes[n]
|
|
448
|
+
n1 = next(x for x in adjacency[t1] if x in env)
|
|
449
|
+
n2 = next(x for x in adjacency[t2] if x in env)
|
|
450
|
+
smi[3] = '@' if self._translate_allene_sign(n, n1, n2) else '@@'
|
|
451
|
+
# tetrahedron
|
|
452
|
+
elif atom.implicit_hydrogens and next(x for x in adjacency) == n:
|
|
453
|
+
# first atom in smiles has reversed chiral mark
|
|
454
|
+
smi[3] = '@@' if self._translate_tetrahedron_sign(n, adjacency[n]) else '@'
|
|
455
|
+
else:
|
|
456
|
+
smi[3] = '@' if self._translate_tetrahedron_sign(n, adjacency[n]) else '@@'
|
|
457
|
+
|
|
458
|
+
if atom.charge and kwargs.get('charges', True):
|
|
459
|
+
smi[5] = charge_str[atom.charge]
|
|
460
|
+
|
|
461
|
+
if any(smi) or atom.atomic_symbol not in organic_set or atom.is_radical or kwargs.get('hydrogens', False):
|
|
462
|
+
smi[0] = '['
|
|
463
|
+
smi[-1] = ']'
|
|
464
|
+
if atom.implicit_hydrogens == 1:
|
|
465
|
+
smi[4] = 'H'
|
|
466
|
+
elif atom.implicit_hydrogens:
|
|
467
|
+
smi[4] = f'H{atom.implicit_hydrogens}'
|
|
468
|
+
elif atom.hybridization == 4 and atom.implicit_hydrogens and atom in (B, N, P): # pyrrole
|
|
469
|
+
smi[0] = '['
|
|
470
|
+
smi[-1] = ']'
|
|
471
|
+
if atom.implicit_hydrogens == 1:
|
|
472
|
+
smi[4] = 'H'
|
|
473
|
+
else:
|
|
474
|
+
smi[4] = f'H{atom.implicit_hydrogens}'
|
|
475
|
+
elif not atom.implicit_hydrogens and atom in (B, C, P, S) and not self.not_special_connectivity[n]:
|
|
476
|
+
# elemental B, C, P, S
|
|
477
|
+
smi[0] = '['
|
|
478
|
+
smi[-1] = ']'
|
|
479
|
+
elif atom.implicit_hydrogens and atom == P and atom.hybridization != 1:
|
|
480
|
+
smi[0] = '['
|
|
481
|
+
smi[-1] = ']'
|
|
482
|
+
if atom.implicit_hydrogens == 1:
|
|
483
|
+
smi[4] = 'H'
|
|
484
|
+
else:
|
|
485
|
+
smi[4] = f'H{atom.implicit_hydrogens}'
|
|
486
|
+
|
|
487
|
+
if kwargs.get('aromatic', True) and atom.hybridization == 4:
|
|
488
|
+
smi[2] = atom.atomic_symbol.lower()
|
|
489
|
+
else:
|
|
490
|
+
smi[2] = atom.atomic_symbol
|
|
491
|
+
return ''.join(smi)
|
|
492
|
+
|
|
493
|
+
def _format_bond(self, n, m, adjacency, **kwargs):
|
|
494
|
+
if not kwargs.get('bonds', True):
|
|
495
|
+
return ''
|
|
496
|
+
bond = self._bonds[n][m]
|
|
497
|
+
if bond == 4:
|
|
498
|
+
if kwargs.get('aromatic', True):
|
|
499
|
+
return ''
|
|
500
|
+
return ':'
|
|
501
|
+
elif bond == 1: # cis-trans /\
|
|
502
|
+
if kwargs.get('aromatic', True) and self._atoms[n].hybridization == self._atoms[m].hybridization == 4:
|
|
503
|
+
return '-'
|
|
504
|
+
if kwargs.get('stereo', True):
|
|
505
|
+
if 'cache' in adjacency:
|
|
506
|
+
ct_map = adjacency['cache']
|
|
507
|
+
else:
|
|
508
|
+
ct_map = adjacency['cache'] = self.__ct_map(adjacency)
|
|
509
|
+
|
|
510
|
+
if (x := ct_map.get((n, m))) is not None:
|
|
511
|
+
return '/' if x else '\\'
|
|
512
|
+
return ''
|
|
513
|
+
elif bond == 2:
|
|
514
|
+
return '='
|
|
515
|
+
elif bond == 3:
|
|
516
|
+
return '#'
|
|
517
|
+
else: # bond == 8
|
|
518
|
+
return '~'
|
|
519
|
+
|
|
520
|
+
def __ct_map(self, adjacency):
|
|
521
|
+
stereo_bonds = {n for n, mb in self._bonds.items() if any(b.stereo is not None for m, b in mb.items())}
|
|
522
|
+
if not stereo_bonds:
|
|
523
|
+
return {}
|
|
524
|
+
ct_map = {}
|
|
525
|
+
sct = self.stereogenic_cis_trans
|
|
526
|
+
ctc = self._stereo_cis_trans_centers
|
|
527
|
+
ctt = self._stereo_cis_trans_terminals
|
|
528
|
+
ctcp = self._stereo_cis_trans_counterpart
|
|
529
|
+
|
|
530
|
+
seen = set()
|
|
531
|
+
for k, vs in adjacency.items():
|
|
532
|
+
seen.add(k)
|
|
533
|
+
if (cs := ctc.get(k)) and stereo_bonds.issuperset(cs):
|
|
534
|
+
env = sct[ctt[k]]
|
|
535
|
+
for v in vs:
|
|
536
|
+
if v in env:
|
|
537
|
+
if (k, v) in ct_map:
|
|
538
|
+
continue
|
|
539
|
+
elif x := ct_map.get(k): # second substituent of C=
|
|
540
|
+
s = ct_map[(k, x)]
|
|
541
|
+
ct_map[(k, v)] = not s # X/C(/R)=, C(\X)(/R)=, C(=C(\X)/R)=C=
|
|
542
|
+
ct_map[(v, k)] = s
|
|
543
|
+
if y := ctc.get(v): # =C(\X)/R=, C(\X)(/R=)=
|
|
544
|
+
ct_map[v] = k
|
|
545
|
+
seen.add(y)
|
|
546
|
+
elif cs in seen:
|
|
547
|
+
o = ctcp[k]
|
|
548
|
+
on = ct_map[o]
|
|
549
|
+
s = ct_map[(o, on)]
|
|
550
|
+
if not self._translate_cis_trans_sign(k, o, v, on):
|
|
551
|
+
s = not s
|
|
552
|
+
ct_map[(k, v)] = s
|
|
553
|
+
ct_map[k] = v
|
|
554
|
+
ct_map[(v, k)] = not s # C/R=, R\1...C/1
|
|
555
|
+
if y := ctc.get(v):
|
|
556
|
+
ct_map[v] = k
|
|
557
|
+
seen.add(y)
|
|
558
|
+
else: # left entry to double bond
|
|
559
|
+
if y := ctc.get(v): # 1,3-diene case
|
|
560
|
+
ct_map[v] = k
|
|
561
|
+
seen.add(y)
|
|
562
|
+
ct_map[(v, k)] = True # R/C=, C\1=...R/1, C(/R=)=, C(=C(/R=))=C=
|
|
563
|
+
ct_map[(k, v)] = False # first DOWN
|
|
564
|
+
ct_map[k] = v
|
|
565
|
+
seen.add(cs)
|
|
566
|
+
return ct_map
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
class CGRSmiles(Smiles):
|
|
570
|
+
__slots__ = ()
|
|
571
|
+
|
|
572
|
+
def _smiles_order(self, stereo=True):
|
|
573
|
+
return self.atoms_order.__getitem__
|
|
574
|
+
|
|
575
|
+
def _format_atom(self, n, adjacency, **kwargs):
|
|
576
|
+
atom = self._atoms[n]
|
|
577
|
+
if atom.isotope:
|
|
578
|
+
smi = [str(atom.isotope), atom.atomic_symbol]
|
|
579
|
+
else:
|
|
580
|
+
smi = [atom.atomic_symbol]
|
|
581
|
+
|
|
582
|
+
if atom.charge or atom.p_charge:
|
|
583
|
+
smi.append(dyn_charge_str[(atom.charge, atom.p_charge)])
|
|
584
|
+
if atom.is_radical or atom.p_is_radical:
|
|
585
|
+
smi.append(dyn_radical_str[(atom.is_radical, atom.p_is_radical)])
|
|
586
|
+
|
|
587
|
+
if len(smi) != 1 or atom.atomic_symbol not in organic_set:
|
|
588
|
+
smi.insert(0, '[')
|
|
589
|
+
smi.append(']')
|
|
590
|
+
return ''.join(smi)
|
|
591
|
+
|
|
592
|
+
def _format_bond(self, n, m, adjacency, **kwargs):
|
|
593
|
+
bond = self._bonds[n][m]
|
|
594
|
+
return dyn_order_str[(bond.order, bond.p_order)]
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
__all__ = ['MoleculeSmiles', 'CGRSmiles']
|
|
@@ -0,0 +1,30 @@
|
|
|
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 .molecule import *
|
|
20
|
+
from .reaction import *
|
|
21
|
+
from .resonance import *
|
|
22
|
+
from .salts import *
|
|
23
|
+
from .saturation import *
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StandardizeMolecule(Standardize, Resonance, Saturation, Salts):
|
|
27
|
+
__slots__ = ()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = ['StandardizeMolecule', 'StandardizeReaction']
|