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,1215 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2019-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 functools import cached_property
|
|
21
|
+
from itertools import combinations, product
|
|
22
|
+
from logging import getLogger, INFO
|
|
23
|
+
from .morgan import _morgan
|
|
24
|
+
from ..exceptions import AtomNotFound, IsChiral, NotChiral
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
logger = getLogger('chython.stereo')
|
|
28
|
+
logger.setLevel(INFO)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# atomic number constants
|
|
32
|
+
H = 1
|
|
33
|
+
C = 6
|
|
34
|
+
|
|
35
|
+
# 1 2
|
|
36
|
+
# \ |
|
|
37
|
+
# \|
|
|
38
|
+
# n---3
|
|
39
|
+
# /
|
|
40
|
+
# /
|
|
41
|
+
# 0
|
|
42
|
+
_tetrahedron_translate = {(0, 1, 2): False, (1, 2, 0): False, (2, 0, 1): False,
|
|
43
|
+
(0, 2, 1): True, (1, 0, 2): True, (2, 1, 0): True,
|
|
44
|
+
(0, 3, 1): False, (3, 1, 0): False, (1, 0, 3): False,
|
|
45
|
+
(0, 1, 3): True, (1, 3, 0): True, (3, 0, 1): True,
|
|
46
|
+
(0, 2, 3): False, (2, 3, 0): False, (3, 0, 2): False,
|
|
47
|
+
(0, 3, 2): True, (3, 2, 0): True, (2, 0, 3): True,
|
|
48
|
+
(1, 3, 2): False, (3, 2, 1): False, (2, 1, 3): False,
|
|
49
|
+
(1, 2, 3): True, (2, 3, 1): True, (3, 1, 2): True}
|
|
50
|
+
# 2 1
|
|
51
|
+
# \ /
|
|
52
|
+
# n---m
|
|
53
|
+
# / \
|
|
54
|
+
# 0 3
|
|
55
|
+
_alkene_translate = {(0, 1): False, (1, 0): False, (0, 3): True, (3, 0): True,
|
|
56
|
+
(2, 3): False, (3, 2): False, (2, 1): True, (1, 2): True}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _pyramid_sign(n, u, v, w):
|
|
60
|
+
#
|
|
61
|
+
# | n /
|
|
62
|
+
# | |\
|
|
63
|
+
# | | \
|
|
64
|
+
# | /| \
|
|
65
|
+
# | / u---v
|
|
66
|
+
# |/___\_/___
|
|
67
|
+
# w
|
|
68
|
+
#
|
|
69
|
+
nx, ny, nz = n
|
|
70
|
+
ux, uy, uz = u
|
|
71
|
+
vx, vy, vz = v
|
|
72
|
+
wx, wy, wz = w
|
|
73
|
+
|
|
74
|
+
q1x = ux - nx
|
|
75
|
+
q1y = uy - ny
|
|
76
|
+
q1z = uz - nz
|
|
77
|
+
q2x = vx - nx
|
|
78
|
+
q2y = vy - ny
|
|
79
|
+
q2z = vz - nz
|
|
80
|
+
q3x = wx - nx
|
|
81
|
+
q3y = wy - ny
|
|
82
|
+
q3z = wz - nz
|
|
83
|
+
|
|
84
|
+
vol = q1x * (q2y * q3z - q2z * q3y) + q1y * (q2z * q3x - q2x * q3z) + q1z * (q2x * q3y - q2y * q3x)
|
|
85
|
+
if vol > 0:
|
|
86
|
+
return 1
|
|
87
|
+
elif vol < 0:
|
|
88
|
+
return -1
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _cis_trans_sign(n, u, v, w):
|
|
93
|
+
# n w
|
|
94
|
+
# \ /
|
|
95
|
+
# u--v
|
|
96
|
+
# / \
|
|
97
|
+
# x x
|
|
98
|
+
nx, ny = n
|
|
99
|
+
ux, uy = u
|
|
100
|
+
vx, vy = v
|
|
101
|
+
wx, wy = w
|
|
102
|
+
|
|
103
|
+
q1x = ux - nx
|
|
104
|
+
q1y = uy - ny
|
|
105
|
+
q2x = vx - ux
|
|
106
|
+
q2y = vy - uy
|
|
107
|
+
q3x = wx - vx
|
|
108
|
+
q3y = wy - vy
|
|
109
|
+
|
|
110
|
+
# cross vectors
|
|
111
|
+
q1q2z = q1x * q2y - q1y * q2x
|
|
112
|
+
q2q3z = q2x * q3y - q2y * q3x
|
|
113
|
+
|
|
114
|
+
dot = q1q2z * q2q3z
|
|
115
|
+
if dot > 0:
|
|
116
|
+
return 1
|
|
117
|
+
elif dot < 0:
|
|
118
|
+
return -1
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _allene_sign(mark, u, v, w):
|
|
123
|
+
# n w
|
|
124
|
+
# | /
|
|
125
|
+
# u--v
|
|
126
|
+
ux, uy = u
|
|
127
|
+
vx, vy = v
|
|
128
|
+
wx, wy = w
|
|
129
|
+
|
|
130
|
+
q2x = vx - ux
|
|
131
|
+
q2y = vy - uy
|
|
132
|
+
q3x = wx - vx
|
|
133
|
+
q3y = wy - vy
|
|
134
|
+
|
|
135
|
+
# cross vectors
|
|
136
|
+
q2q3z = q2x * q3y - q2y * q3x
|
|
137
|
+
|
|
138
|
+
dot = -mark * q2q3z
|
|
139
|
+
if dot > 0:
|
|
140
|
+
return 1
|
|
141
|
+
elif dot < 0:
|
|
142
|
+
return -1
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class MoleculeStereo:
|
|
147
|
+
__slots__ = ()
|
|
148
|
+
|
|
149
|
+
def clean_stereo(self):
|
|
150
|
+
"""
|
|
151
|
+
Remove stereo data.
|
|
152
|
+
"""
|
|
153
|
+
for _, a in self.atoms():
|
|
154
|
+
a._stereo = None
|
|
155
|
+
for *_, b in self.bonds():
|
|
156
|
+
b._stereo = None
|
|
157
|
+
self.flush_cache(keep_sssr=True, keep_components=True, keep_special_connectivity=True)
|
|
158
|
+
|
|
159
|
+
@cached_property
|
|
160
|
+
def tetrahedrons(self) -> tuple[int, ...]:
|
|
161
|
+
"""
|
|
162
|
+
Carbon sp3 atom numbers.
|
|
163
|
+
"""
|
|
164
|
+
tetra = []
|
|
165
|
+
for n, atom in self.atoms():
|
|
166
|
+
if atom == C and not atom.charge and not atom.is_radical:
|
|
167
|
+
env = self._bonds[n]
|
|
168
|
+
if all(b == 1 for b in env.values()):
|
|
169
|
+
if sum(int(b) for b in env.values()) > 4:
|
|
170
|
+
continue
|
|
171
|
+
tetra.append(n)
|
|
172
|
+
return tuple(tetra)
|
|
173
|
+
|
|
174
|
+
@cached_property
|
|
175
|
+
def cumulenes(self) -> list[tuple[int, ...]]:
|
|
176
|
+
"""
|
|
177
|
+
All double-bonds chains (e.g. alkenes, allenes, cumulenes).
|
|
178
|
+
"""
|
|
179
|
+
atoms = self._atoms
|
|
180
|
+
bonds = self._bonds
|
|
181
|
+
|
|
182
|
+
adj = defaultdict(set) # double bonds adjacency matrix
|
|
183
|
+
for n, atom in atoms.items():
|
|
184
|
+
if atom.is_forming_double_bonds:
|
|
185
|
+
adj_n = adj[n].add
|
|
186
|
+
for m, bond in bonds[n].items():
|
|
187
|
+
if bond == 2 and atoms[m].is_forming_double_bonds:
|
|
188
|
+
adj_n(m)
|
|
189
|
+
if not adj:
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
terminals = [x for x, y in adj.items() if len(y) == 1] # list to keep atoms order!
|
|
193
|
+
cumulenes = []
|
|
194
|
+
while terminals:
|
|
195
|
+
n = terminals.pop(0)
|
|
196
|
+
m = adj[n].pop()
|
|
197
|
+
path = [n, m]
|
|
198
|
+
while m not in terminals:
|
|
199
|
+
if len(bonds[m]) > 2: # not cumulene. SO3, SO4- etc.
|
|
200
|
+
cumulenes.extend(zip(path, path[1:])) # keep single double bonds instead of cumulene chain.
|
|
201
|
+
break
|
|
202
|
+
adj_m = adj[m]
|
|
203
|
+
adj_m.discard(n)
|
|
204
|
+
n, m = m, adj_m.pop()
|
|
205
|
+
path.append(m)
|
|
206
|
+
else:
|
|
207
|
+
terminals.remove(m)
|
|
208
|
+
adj[m].pop()
|
|
209
|
+
cumulenes.append(tuple(path))
|
|
210
|
+
return cumulenes
|
|
211
|
+
|
|
212
|
+
@cached_property
|
|
213
|
+
def stereogenic_tetrahedrons(self) -> dict[int, tuple[int, int, int] | tuple[int, int, int, int]]:
|
|
214
|
+
"""
|
|
215
|
+
Tetrahedrons which contains at least 3 non-hydrogen neighbors and corresponding neighbors order.
|
|
216
|
+
"""
|
|
217
|
+
# 2
|
|
218
|
+
# |
|
|
219
|
+
# 1--K--3
|
|
220
|
+
# |
|
|
221
|
+
# 4?
|
|
222
|
+
atoms = self._atoms
|
|
223
|
+
bonds = self._bonds
|
|
224
|
+
tetrahedrons = {}
|
|
225
|
+
for n in self.tetrahedrons:
|
|
226
|
+
if any(not atoms[x].is_forming_single_bonds for x in bonds[n]):
|
|
227
|
+
continue # skip metal-carbon complexes
|
|
228
|
+
env = tuple(x for x in bonds[n] if atoms[x] != H)
|
|
229
|
+
if len(env) in (3, 4):
|
|
230
|
+
tetrahedrons[n] = env
|
|
231
|
+
return tetrahedrons
|
|
232
|
+
|
|
233
|
+
@cached_property
|
|
234
|
+
def stereogenic_cumulenes(self) -> dict[tuple[int, ...], tuple[int, int, int | None, int | None]]:
|
|
235
|
+
"""
|
|
236
|
+
Cumulenes which contains at least one non-hydrogen neighbor on both ends and corresponding neighbors order.
|
|
237
|
+
"""
|
|
238
|
+
# 5 4
|
|
239
|
+
# \ /
|
|
240
|
+
# 2---3
|
|
241
|
+
# / \
|
|
242
|
+
# 1 6
|
|
243
|
+
bonds = self._bonds
|
|
244
|
+
atoms = self._atoms
|
|
245
|
+
cumulenes = {}
|
|
246
|
+
for path in self.cumulenes:
|
|
247
|
+
nf = bonds[path[0]]
|
|
248
|
+
nl = bonds[path[-1]]
|
|
249
|
+
n1, m1 = path[1], path[-2]
|
|
250
|
+
if any(b == 3 or not atoms[m].is_forming_single_bonds and b != 8
|
|
251
|
+
for m, b in nf.items() if m != n1):
|
|
252
|
+
continue # skip X=C=C structures and metal-carbon complexes
|
|
253
|
+
if any(b == 3 or not atoms[m].is_forming_single_bonds and b != 8
|
|
254
|
+
for m, b in nl.items() if m != m1):
|
|
255
|
+
continue # skip X=C=C structures and metal-carbon complexes
|
|
256
|
+
nn = [x for x, b in nf.items() if x != n1 and atoms[x] != H and b != 8]
|
|
257
|
+
mn = [x for x, b in nl.items() if x != m1 and atoms[x] != H and b != 8]
|
|
258
|
+
if nn and mn:
|
|
259
|
+
sn = nn[1] if len(nn) == 2 else None
|
|
260
|
+
sm = mn[1] if len(mn) == 2 else None
|
|
261
|
+
cumulenes[path] = (nn[0], mn[0], sn, sm)
|
|
262
|
+
return cumulenes
|
|
263
|
+
|
|
264
|
+
@cached_property
|
|
265
|
+
def stereogenic_allenes(self) -> dict[int, tuple[int, int, int | None, int | None]]:
|
|
266
|
+
"""
|
|
267
|
+
Allenes which contains at least one non-hydrogen neighbor on both ends and corresponding neighbors order.
|
|
268
|
+
"""
|
|
269
|
+
return {path[len(path) // 2]: env for path, env in self.stereogenic_cumulenes.items() if len(path) % 2}
|
|
270
|
+
|
|
271
|
+
@cached_property
|
|
272
|
+
def stereogenic_cis_trans(self) -> dict[tuple[int, int], tuple[int, int, int | None, int | None]]:
|
|
273
|
+
"""
|
|
274
|
+
Cis-trans bonds which contains at least one non-hydrogen neighbor on both ends and corresponding neighbors order.
|
|
275
|
+
"""
|
|
276
|
+
stereo = {}
|
|
277
|
+
for path, env in self.stereogenic_cumulenes.items():
|
|
278
|
+
if len(path) % 2:
|
|
279
|
+
continue
|
|
280
|
+
stereo[(path[0], path[-1])] = env
|
|
281
|
+
return stereo
|
|
282
|
+
|
|
283
|
+
@cached_property
|
|
284
|
+
def ring_tetrahedrons(self) -> dict[int, tuple[int, int] | tuple[int] | tuple]:
|
|
285
|
+
"""
|
|
286
|
+
Tetrahedrons in rings, except ring-linkers. Values are non-ring atoms.
|
|
287
|
+
"""
|
|
288
|
+
out = {}
|
|
289
|
+
atoms_rings = self.atoms_rings
|
|
290
|
+
tetrahedrons = self.stereogenic_tetrahedrons
|
|
291
|
+
points = self.rings_linker_tetrahedrons
|
|
292
|
+
environment = self.not_special_connectivity
|
|
293
|
+
for n, r in atoms_rings.items():
|
|
294
|
+
if n in tetrahedrons and n not in points:
|
|
295
|
+
out[n] = tuple(environment[n].difference(atoms_rings))
|
|
296
|
+
return out
|
|
297
|
+
|
|
298
|
+
@cached_property
|
|
299
|
+
def rings_linker_tetrahedrons(self) -> dict[int, tuple[int, int, int, int]]:
|
|
300
|
+
"""
|
|
301
|
+
A dictionary where the keys are tetrahedron atoms shared between two rings (not condensed rings) and the values
|
|
302
|
+
are tuples representing their neighbors in the first and second rings respectively.
|
|
303
|
+
"""
|
|
304
|
+
out = {}
|
|
305
|
+
tetrahedrons = self.stereogenic_tetrahedrons
|
|
306
|
+
for n, r in self.atoms_rings.items():
|
|
307
|
+
if n in tetrahedrons:
|
|
308
|
+
for nr, mr in combinations(r, 2):
|
|
309
|
+
if len(set(nr).intersection(mr)) == 1:
|
|
310
|
+
ni = nr.index(n)
|
|
311
|
+
mi = mr.index(n)
|
|
312
|
+
out[n] = (nr[ni - 1], nr[ni - len(nr) + 1], mr[mi - 1], mr[mi - len(mr) + 1])
|
|
313
|
+
break
|
|
314
|
+
return out
|
|
315
|
+
|
|
316
|
+
@cached_property
|
|
317
|
+
def ring_cumulenes_terminals(self) -> set[tuple[int, int]]:
|
|
318
|
+
"""
|
|
319
|
+
Terminal atoms of inside ring cumulenes.
|
|
320
|
+
"""
|
|
321
|
+
out = set()
|
|
322
|
+
ar = self.atoms_rings
|
|
323
|
+
for n, *_, m in self.stereogenic_cumulenes:
|
|
324
|
+
if n in ar and m in ar and not set(ar[n]).isdisjoint(ar[m]):
|
|
325
|
+
out.add((n, m))
|
|
326
|
+
return out
|
|
327
|
+
|
|
328
|
+
@cached_property
|
|
329
|
+
def rings_linker_cumulenes_terminals(self) -> dict[tuple[int, int], tuple[int, int, int, int]]:
|
|
330
|
+
"""
|
|
331
|
+
Terminal atoms of cumulenes connecting two rings. Values are neighbors in first and second rings.
|
|
332
|
+
"""
|
|
333
|
+
out = {}
|
|
334
|
+
ar = self.atoms_rings
|
|
335
|
+
chord = self.ring_cumulenes_terminals
|
|
336
|
+
for (n, *_, m), (n1, m1, n2, m2) in self.stereogenic_cumulenes.items():
|
|
337
|
+
if n in ar and m in ar and (n, m) not in chord:
|
|
338
|
+
out[(n, m)] = (n1, n2, m1, m2)
|
|
339
|
+
return out
|
|
340
|
+
|
|
341
|
+
@cached_property
|
|
342
|
+
def ring_attached_cumulenes(self) -> dict[tuple[int, int], tuple[int, int] | tuple[int]]:
|
|
343
|
+
"""
|
|
344
|
+
Cumulenes attached to rings from one side. Values are out of ring neighbor atoms.
|
|
345
|
+
"""
|
|
346
|
+
ar = self.atoms_rings
|
|
347
|
+
out = {}
|
|
348
|
+
for (n, *_, m), (n1, m1, n2, m2) in self.stereogenic_cumulenes.items():
|
|
349
|
+
if n in ar:
|
|
350
|
+
if m in ar:
|
|
351
|
+
continue
|
|
352
|
+
if m2:
|
|
353
|
+
out[(n, m)] = (m1, m2)
|
|
354
|
+
else:
|
|
355
|
+
out[(n, m)] = (m1,)
|
|
356
|
+
elif m in ar:
|
|
357
|
+
if n2:
|
|
358
|
+
out[(n, m)] = (n1, n2)
|
|
359
|
+
else:
|
|
360
|
+
out[(n, m)] = (n1,)
|
|
361
|
+
return out
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def chiral_tetrahedrons(self) -> set[int]:
|
|
365
|
+
"""
|
|
366
|
+
Chiral tetrahedrons except already labeled ones.
|
|
367
|
+
"""
|
|
368
|
+
return self.__chiral_centers[0]
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def chiral_cis_trans(self) -> set[tuple[int, int]]:
|
|
372
|
+
"""
|
|
373
|
+
Chiral cis-trans bonds except already labeled ones.
|
|
374
|
+
"""
|
|
375
|
+
return self.__chiral_centers[1]
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def chiral_allenes(self) -> set[int]:
|
|
379
|
+
"""
|
|
380
|
+
Chiral allenes except already labeled ones.
|
|
381
|
+
"""
|
|
382
|
+
return self.__chiral_centers[2]
|
|
383
|
+
|
|
384
|
+
def add_wedge(self, n: int, m: int, mark: int, *, clean_cache=True):
|
|
385
|
+
"""
|
|
386
|
+
Add stereo data by wedge notation of bonds. Use it for tetrahedrons of allenes.
|
|
387
|
+
|
|
388
|
+
:param n: number of atom from which wedge bond started
|
|
389
|
+
:param m: number of atom to which wedge bond coming
|
|
390
|
+
:param mark: up bond is 1, down is -1
|
|
391
|
+
"""
|
|
392
|
+
atoms = self._atoms
|
|
393
|
+
if n not in atoms or m not in atoms or m not in self._bonds[n]:
|
|
394
|
+
raise AtomNotFound
|
|
395
|
+
elif atoms[n].stereo is not None:
|
|
396
|
+
raise IsChiral
|
|
397
|
+
elif c := self._stereo_allenes_centers.get(n):
|
|
398
|
+
# allenes
|
|
399
|
+
if atoms[c].stereo is not None:
|
|
400
|
+
raise IsChiral
|
|
401
|
+
elif c not in self.chiral_allenes:
|
|
402
|
+
raise NotChiral
|
|
403
|
+
|
|
404
|
+
t1, t2 = self._stereo_allenes_terminals[c]
|
|
405
|
+
order = self.stereogenic_allenes[c]
|
|
406
|
+
if atoms[m] == H:
|
|
407
|
+
if t1 == n:
|
|
408
|
+
m1 = order[1]
|
|
409
|
+
else:
|
|
410
|
+
t1, t2 = t2, t1
|
|
411
|
+
m1 = order[0]
|
|
412
|
+
r = True
|
|
413
|
+
else:
|
|
414
|
+
w = order.index(m)
|
|
415
|
+
if w == 0:
|
|
416
|
+
m1 = order[1]
|
|
417
|
+
r = False
|
|
418
|
+
elif w == 1:
|
|
419
|
+
m1 = order[0]
|
|
420
|
+
t1, t2 = t2, t1
|
|
421
|
+
r = False
|
|
422
|
+
elif w == 2:
|
|
423
|
+
m1 = order[1]
|
|
424
|
+
r = True
|
|
425
|
+
else:
|
|
426
|
+
m1 = order[0]
|
|
427
|
+
t1, t2 = t2, t1
|
|
428
|
+
r = True
|
|
429
|
+
if s := _allene_sign(mark, atoms[t1].xy, atoms[t2].xy, atoms[m1].xy):
|
|
430
|
+
atoms[c]._stereo = s < 0 if r else s > 0
|
|
431
|
+
if clean_cache:
|
|
432
|
+
self.flush_cache(keep_sssr=True, keep_components=True, keep_special_connectivity=True)
|
|
433
|
+
# tetrahedrons
|
|
434
|
+
elif n in self.chiral_tetrahedrons:
|
|
435
|
+
th = self.stereogenic_tetrahedrons[n]
|
|
436
|
+
am = atoms[m]
|
|
437
|
+
if am == H:
|
|
438
|
+
order = []
|
|
439
|
+
for x in th:
|
|
440
|
+
ax = atoms[x]
|
|
441
|
+
order.append((ax.x, ax.y, 0))
|
|
442
|
+
s = _pyramid_sign((am.x, am.y, mark), *order)
|
|
443
|
+
else:
|
|
444
|
+
order = []
|
|
445
|
+
for x in th:
|
|
446
|
+
ax = atoms[x]
|
|
447
|
+
order.append((ax.x, ax.y, mark if x == m else 0))
|
|
448
|
+
if len(order) == 3:
|
|
449
|
+
if len(self._bonds[n]) == 4: # explicit hydrogen
|
|
450
|
+
x = next(x for x in self._bonds[n] if x not in th)
|
|
451
|
+
ax = atoms[x]
|
|
452
|
+
s = _pyramid_sign((ax.x, ax.y, 0), *order)
|
|
453
|
+
else:
|
|
454
|
+
an = atoms[n]
|
|
455
|
+
s = _pyramid_sign((an.x, an.y, 0), *order)
|
|
456
|
+
else:
|
|
457
|
+
s = _pyramid_sign(order[-1], *order[:3])
|
|
458
|
+
if s:
|
|
459
|
+
atoms[n]._stereo = s > 0
|
|
460
|
+
if clean_cache:
|
|
461
|
+
self.flush_cache(keep_components=True, keep_sssr=True, keep_special_connectivity=True)
|
|
462
|
+
else:
|
|
463
|
+
raise NotChiral
|
|
464
|
+
|
|
465
|
+
def calculate_cis_trans_from_2d(self, *, clean_cache=True):
|
|
466
|
+
"""
|
|
467
|
+
Calculate cis-trans stereo bonds from given 2d coordinates. Unusable for SMILES and INCHI.
|
|
468
|
+
"""
|
|
469
|
+
atoms = self._atoms
|
|
470
|
+
flag = False
|
|
471
|
+
while self.chiral_cis_trans:
|
|
472
|
+
stereo = False
|
|
473
|
+
for nm in self.chiral_cis_trans:
|
|
474
|
+
n, m = nm
|
|
475
|
+
n1, m1, *_ = self.stereogenic_cis_trans[nm]
|
|
476
|
+
s = _cis_trans_sign(atoms[n1].xy, atoms[n].xy, atoms[m].xy, atoms[m1].xy)
|
|
477
|
+
if s:
|
|
478
|
+
stereo = True
|
|
479
|
+
i, j = self._stereo_cis_trans_centers[n]
|
|
480
|
+
self._bonds[i][j]._stereo = s > 0
|
|
481
|
+
if stereo:
|
|
482
|
+
flag = True
|
|
483
|
+
self.flush_stereo_cache()
|
|
484
|
+
else:
|
|
485
|
+
break
|
|
486
|
+
if flag and clean_cache:
|
|
487
|
+
self.flush_cache(keep_components=True, keep_sssr=True, keep_special_connectivity=True)
|
|
488
|
+
|
|
489
|
+
def add_atom_stereo(self, n: int, env: tuple[int, ...], mark: bool, *, clean_cache=True):
|
|
490
|
+
"""
|
|
491
|
+
Add stereo data for specified neighbors bypass. Use it for tetrahedrons or allenes.
|
|
492
|
+
|
|
493
|
+
:param n: number of tetrahedron atom or central atom of allene.
|
|
494
|
+
:param env: numbers of atoms with specified bypass
|
|
495
|
+
:param mark: clockwise or anti bypass.
|
|
496
|
+
|
|
497
|
+
See <https://www.daylight.com/dayhtml/doc/theory/theory.smiles.html> and <http://opensmiles.org/opensmiles.html>
|
|
498
|
+
"""
|
|
499
|
+
try:
|
|
500
|
+
atom = self._atoms[n]
|
|
501
|
+
except KeyError:
|
|
502
|
+
raise AtomNotFound
|
|
503
|
+
if atom.stereo is not None:
|
|
504
|
+
raise IsChiral
|
|
505
|
+
if not isinstance(mark, bool):
|
|
506
|
+
raise TypeError('stereo mark should be bool')
|
|
507
|
+
|
|
508
|
+
if n in self.chiral_tetrahedrons:
|
|
509
|
+
atom._stereo = self._translate_tetrahedron_sign(n, env, mark)
|
|
510
|
+
if clean_cache:
|
|
511
|
+
self.flush_cache(keep_components=True, keep_sssr=True, keep_special_connectivity=True)
|
|
512
|
+
elif n in self.chiral_allenes:
|
|
513
|
+
atom._stereo = self._translate_allene_sign(n, *env, mark)
|
|
514
|
+
if clean_cache:
|
|
515
|
+
self.flush_cache(keep_components=True, keep_sssr=True, keep_special_connectivity=True)
|
|
516
|
+
else: # only tetrahedrons supported
|
|
517
|
+
raise NotChiral
|
|
518
|
+
|
|
519
|
+
def add_cis_trans_stereo(self, n: int, m: int, n1: int, n2: int, mark: bool, *, clean_cache=True):
|
|
520
|
+
"""
|
|
521
|
+
Add stereo data to cis-trans double bonds (not allenes).
|
|
522
|
+
|
|
523
|
+
n1/n=m/n2
|
|
524
|
+
|
|
525
|
+
:param n: number of starting atom of double bonds chain (alkenes of cumulenes)
|
|
526
|
+
:param m: number of ending atom of double bonds chain (alkenes of cumulenes)
|
|
527
|
+
:param n1: number of neighboring atom of starting atom
|
|
528
|
+
:param n2: number of neighboring atom of ending atom
|
|
529
|
+
:param mark: cis or trans
|
|
530
|
+
|
|
531
|
+
See <https://www.daylight.com/dayhtml/doc/theory/theory.smiles.html> and <http://opensmiles.org/opensmiles.html
|
|
532
|
+
"""
|
|
533
|
+
atoms = self._atoms
|
|
534
|
+
if n not in atoms or m not in atoms or n1 not in atoms or n2 not in atoms:
|
|
535
|
+
raise AtomNotFound
|
|
536
|
+
if not isinstance(mark, bool):
|
|
537
|
+
raise TypeError('stereo mark should be bool')
|
|
538
|
+
|
|
539
|
+
if n not in self._stereo_cis_trans_counterpart or self._stereo_cis_trans_counterpart[n] != m:
|
|
540
|
+
raise NotChiral
|
|
541
|
+
i, j = self._stereo_cis_trans_centers[n]
|
|
542
|
+
if self._bonds[i][j].stereo is not None:
|
|
543
|
+
raise IsChiral
|
|
544
|
+
|
|
545
|
+
if (n, m) in self.chiral_cis_trans:
|
|
546
|
+
self._bonds[i][j]._stereo = self._translate_cis_trans_sign(n, m, n1, n2, mark)
|
|
547
|
+
if clean_cache:
|
|
548
|
+
self.flush_cache(keep_components=True, keep_sssr=True, keep_special_connectivity=True)
|
|
549
|
+
elif (m, n) in self.chiral_cis_trans:
|
|
550
|
+
self._bonds[i][j]._stereo = self._translate_cis_trans_sign(m, n, n2, n1, mark)
|
|
551
|
+
if clean_cache:
|
|
552
|
+
self.flush_cache(keep_components=True, keep_sssr=True, keep_special_connectivity=True)
|
|
553
|
+
else:
|
|
554
|
+
raise NotChiral
|
|
555
|
+
|
|
556
|
+
def flush_stereo_cache(self):
|
|
557
|
+
"""
|
|
558
|
+
Flush chiral morgan and chiral centers cache.
|
|
559
|
+
"""
|
|
560
|
+
self.__dict__.pop('_chiral_morgan', None)
|
|
561
|
+
self.__dict__.pop('_MoleculeStereo__chiral_centers', None)
|
|
562
|
+
|
|
563
|
+
def fix_stereo(self):
|
|
564
|
+
"""
|
|
565
|
+
Reset stereo marks.
|
|
566
|
+
"""
|
|
567
|
+
stereo_tetrahedrons = self.stereogenic_tetrahedrons
|
|
568
|
+
stereo_allenes = self.stereogenic_allenes
|
|
569
|
+
stereo_cis_trans = self._stereo_cis_trans_terminals
|
|
570
|
+
atoms_stereo = []
|
|
571
|
+
allenes_stereo = []
|
|
572
|
+
cis_trans_stereo = []
|
|
573
|
+
for n, a in self.atoms():
|
|
574
|
+
if a.stereo is None:
|
|
575
|
+
continue
|
|
576
|
+
elif n in stereo_tetrahedrons:
|
|
577
|
+
atoms_stereo.append((n, a, a.stereo))
|
|
578
|
+
elif n in stereo_allenes:
|
|
579
|
+
allenes_stereo.append((n, a, a.stereo))
|
|
580
|
+
a._stereo = None # flush stereo label
|
|
581
|
+
|
|
582
|
+
for n, m, b in self.bonds():
|
|
583
|
+
if b.stereo is None:
|
|
584
|
+
continue
|
|
585
|
+
elif (ta := stereo_cis_trans.get(n)) and ta == stereo_cis_trans.get(m):
|
|
586
|
+
cis_trans_stereo.append((ta, b, b.stereo))
|
|
587
|
+
b._stereo = None # flush stereo label
|
|
588
|
+
self.flush_stereo_cache()
|
|
589
|
+
|
|
590
|
+
old_stereo = len(atoms_stereo) + len(allenes_stereo) + len(cis_trans_stereo)
|
|
591
|
+
while old_stereo:
|
|
592
|
+
chiral_tetrahedrons = self.chiral_tetrahedrons
|
|
593
|
+
chiral_allenes = self.chiral_allenes
|
|
594
|
+
chiral_cis_trans = self.chiral_cis_trans
|
|
595
|
+
|
|
596
|
+
# filter out resolved
|
|
597
|
+
tmp = []
|
|
598
|
+
for n, a, s in atoms_stereo:
|
|
599
|
+
if n in chiral_tetrahedrons:
|
|
600
|
+
a._stereo = s # restore stereo
|
|
601
|
+
else:
|
|
602
|
+
tmp.append((n, a, s))
|
|
603
|
+
atoms_stereo = tmp
|
|
604
|
+
|
|
605
|
+
tmp = []
|
|
606
|
+
for n, a, s in allenes_stereo:
|
|
607
|
+
if n in chiral_allenes:
|
|
608
|
+
a._stereo = s # restore stereo
|
|
609
|
+
else:
|
|
610
|
+
tmp.append((n, a, s))
|
|
611
|
+
allenes_stereo = tmp
|
|
612
|
+
|
|
613
|
+
tmp = []
|
|
614
|
+
for ta, b, s in cis_trans_stereo:
|
|
615
|
+
if ta in chiral_cis_trans:
|
|
616
|
+
b._stereo = s
|
|
617
|
+
else:
|
|
618
|
+
tmp.append((ta, b, s))
|
|
619
|
+
cis_trans_stereo = tmp
|
|
620
|
+
|
|
621
|
+
fail_stereo = len(atoms_stereo) + len(allenes_stereo) + len(cis_trans_stereo)
|
|
622
|
+
if fail_stereo == old_stereo:
|
|
623
|
+
break
|
|
624
|
+
old_stereo = fail_stereo
|
|
625
|
+
self.flush_stereo_cache()
|
|
626
|
+
|
|
627
|
+
@cached_property
|
|
628
|
+
def _cis_trans_count(self) -> int:
|
|
629
|
+
return sum(b.stereo is not None for *_, b in self.bonds())
|
|
630
|
+
|
|
631
|
+
@cached_property
|
|
632
|
+
def _stereo_cis_trans_centers(self) -> dict[int, tuple[int, int]]:
|
|
633
|
+
"""
|
|
634
|
+
Cis-Trans terminal atoms to cis-trans key mapping. Key is central double bond in a cumulene chain.
|
|
635
|
+
"""
|
|
636
|
+
terminals = {}
|
|
637
|
+
for path in self.stereogenic_cumulenes:
|
|
638
|
+
if len(path) % 2:
|
|
639
|
+
continue
|
|
640
|
+
n, m = path[0], path[-1]
|
|
641
|
+
i = len(path) // 2
|
|
642
|
+
terminals[n] = terminals[m] = (path[i - 1], path[i])
|
|
643
|
+
return terminals
|
|
644
|
+
|
|
645
|
+
@cached_property
|
|
646
|
+
def _stereo_cis_trans_terminals(self) -> dict[int, tuple[int, int]]:
|
|
647
|
+
"""
|
|
648
|
+
Cis-Trans terminal and central atoms to terminal pair mapping.
|
|
649
|
+
"""
|
|
650
|
+
terminals = {}
|
|
651
|
+
for path in self.stereogenic_cumulenes:
|
|
652
|
+
if len(path) % 2:
|
|
653
|
+
continue
|
|
654
|
+
n, m = path[0], path[-1]
|
|
655
|
+
i = len(path) // 2
|
|
656
|
+
terminals[n] = terminals[m] = terminals[path[i]] = terminals[path[i - 1]] = (n, m)
|
|
657
|
+
return terminals
|
|
658
|
+
|
|
659
|
+
@cached_property
|
|
660
|
+
def _stereo_cis_trans_counterpart(self) -> dict[int, int]:
|
|
661
|
+
"""
|
|
662
|
+
Cis-Trans terminal atoms counterparts
|
|
663
|
+
"""
|
|
664
|
+
counterpart = {}
|
|
665
|
+
for path in self.stereogenic_cumulenes:
|
|
666
|
+
if len(path) % 2:
|
|
667
|
+
continue
|
|
668
|
+
n, m = path[0], path[-1]
|
|
669
|
+
counterpart[n] = m
|
|
670
|
+
counterpart[m] = n
|
|
671
|
+
return counterpart
|
|
672
|
+
|
|
673
|
+
@cached_property
|
|
674
|
+
def _stereo_allenes_centers(self) -> dict[int, int]:
|
|
675
|
+
"""
|
|
676
|
+
Allene terminal atom to center mapping
|
|
677
|
+
"""
|
|
678
|
+
terminals = {}
|
|
679
|
+
for c, (n, m) in self._stereo_allenes_terminals.items():
|
|
680
|
+
terminals[n] = terminals[m] = c
|
|
681
|
+
return terminals
|
|
682
|
+
|
|
683
|
+
@cached_property
|
|
684
|
+
def _stereo_allenes_terminals(self) -> dict[int, tuple[int, int]]:
|
|
685
|
+
"""
|
|
686
|
+
Allene center atom to terminals mapping
|
|
687
|
+
"""
|
|
688
|
+
return {path[len(path) // 2]: (path[0], path[-1]) for path in self.stereogenic_cumulenes if len(path) % 2}
|
|
689
|
+
|
|
690
|
+
def _translate_tetrahedron_sign(self, n, env, s=None):
|
|
691
|
+
"""
|
|
692
|
+
Get sign of chiral tetrahedron atom for specified neighbors order
|
|
693
|
+
|
|
694
|
+
:param n: stereo atom
|
|
695
|
+
:param env: neighbors order
|
|
696
|
+
:param s: if None, use existing sign else translate given to molecule
|
|
697
|
+
"""
|
|
698
|
+
if s is None:
|
|
699
|
+
s = self._atoms[n].stereo
|
|
700
|
+
if s is None:
|
|
701
|
+
raise KeyError
|
|
702
|
+
|
|
703
|
+
order = self.stereogenic_tetrahedrons[n]
|
|
704
|
+
if len(order) == 3:
|
|
705
|
+
if len(env) == 4: # hydrogen atom passed to env
|
|
706
|
+
# hydrogen always last in order
|
|
707
|
+
try:
|
|
708
|
+
order = (*order, next(x for x in env if self._atoms[x] == H)) # see translate scheme
|
|
709
|
+
except StopIteration:
|
|
710
|
+
raise KeyError
|
|
711
|
+
elif len(env) != 3: # pyramid or tetrahedron expected
|
|
712
|
+
raise ValueError('invalid atoms list')
|
|
713
|
+
elif len(env) not in (3, 4): # pyramid or tetrahedron expected
|
|
714
|
+
raise ValueError('invalid atoms list')
|
|
715
|
+
|
|
716
|
+
translate = tuple(order.index(x) for x in env[:3])
|
|
717
|
+
if _tetrahedron_translate[translate]:
|
|
718
|
+
return not s
|
|
719
|
+
return s
|
|
720
|
+
|
|
721
|
+
def _translate_cis_trans_sign(self, n, m, nn, nm, s=None):
|
|
722
|
+
"""
|
|
723
|
+
Get sign for specified opposite neighbors
|
|
724
|
+
|
|
725
|
+
:param n: first double bonded atom
|
|
726
|
+
:param m: last double bonded atom
|
|
727
|
+
:param nn: neighbor of first atom
|
|
728
|
+
:param nm: neighbor of last atom
|
|
729
|
+
:param s: if None, use existing sign else translate given to molecule
|
|
730
|
+
"""
|
|
731
|
+
try:
|
|
732
|
+
n0, n1, n2, n3 = self.stereogenic_cis_trans[(n, m)]
|
|
733
|
+
except KeyError:
|
|
734
|
+
n0, n1, n2, n3 = self.stereogenic_cis_trans[(m, n)]
|
|
735
|
+
n, m = m, n # in alkenes sign not order depended
|
|
736
|
+
nn, nm = nm, nn
|
|
737
|
+
|
|
738
|
+
if s is None:
|
|
739
|
+
i, j = self._stereo_cis_trans_centers[n]
|
|
740
|
+
s = self._bonds[i][j].stereo
|
|
741
|
+
if s is None:
|
|
742
|
+
raise KeyError
|
|
743
|
+
|
|
744
|
+
if nn == n0: # same start
|
|
745
|
+
t0 = 0
|
|
746
|
+
if nm == n1:
|
|
747
|
+
t1 = 1
|
|
748
|
+
elif nm == n3 or n3 is None and self._atoms[nm] == H:
|
|
749
|
+
t1 = 3
|
|
750
|
+
else:
|
|
751
|
+
raise KeyError
|
|
752
|
+
elif nn == n1:
|
|
753
|
+
t0 = 1
|
|
754
|
+
if nm == n0:
|
|
755
|
+
t1 = 0
|
|
756
|
+
elif nm == n2 or n2 is None and self._atoms[nm] == H:
|
|
757
|
+
t1 = 2
|
|
758
|
+
else:
|
|
759
|
+
raise KeyError
|
|
760
|
+
elif nn == n2 or n2 is None and self._atoms[nn] == H:
|
|
761
|
+
t0 = 2
|
|
762
|
+
if nm == n1:
|
|
763
|
+
t1 = 1
|
|
764
|
+
elif nm == n3 or n3 is None and self._atoms[nm] == H:
|
|
765
|
+
t1 = 3
|
|
766
|
+
else:
|
|
767
|
+
raise KeyError
|
|
768
|
+
elif nn == n3 or n3 is None and self._atoms[nn] == H:
|
|
769
|
+
t0 = 3
|
|
770
|
+
if nm == n0:
|
|
771
|
+
t1 = 0
|
|
772
|
+
elif nm == n2 or n2 is None and self._atoms[nm] == H:
|
|
773
|
+
t1 = 2
|
|
774
|
+
else:
|
|
775
|
+
raise KeyError
|
|
776
|
+
else:
|
|
777
|
+
raise KeyError
|
|
778
|
+
|
|
779
|
+
if _alkene_translate[(t0, t1)]:
|
|
780
|
+
return not s
|
|
781
|
+
return s
|
|
782
|
+
|
|
783
|
+
def _translate_allene_sign(self, c, nn, nm, s=None):
|
|
784
|
+
"""
|
|
785
|
+
get sign for specified opposite neighbors
|
|
786
|
+
|
|
787
|
+
:param c: central double bonded atom
|
|
788
|
+
:param nn: neighbor of first double bonded atom
|
|
789
|
+
:param nm: neighbor of last double bonded atom
|
|
790
|
+
:param s: if None, use existing sign else translate given to molecule
|
|
791
|
+
"""
|
|
792
|
+
if s is None:
|
|
793
|
+
s = self._atoms[c].stereo
|
|
794
|
+
if s is None:
|
|
795
|
+
raise KeyError
|
|
796
|
+
|
|
797
|
+
n0, n1, n2, n3 = self.stereogenic_allenes[c]
|
|
798
|
+
if nn == n0: # same start
|
|
799
|
+
t0 = 0
|
|
800
|
+
if nm == n1:
|
|
801
|
+
t1 = 1
|
|
802
|
+
elif nm == n3 or n3 is None and self._atoms[nm] == H:
|
|
803
|
+
t1 = 3
|
|
804
|
+
else:
|
|
805
|
+
raise KeyError
|
|
806
|
+
elif nn == n1:
|
|
807
|
+
t0 = 1
|
|
808
|
+
if nm == n0:
|
|
809
|
+
t1 = 0
|
|
810
|
+
elif nm == n2 or n2 is None and self._atoms[nm] == H:
|
|
811
|
+
t1 = 2
|
|
812
|
+
else:
|
|
813
|
+
raise KeyError
|
|
814
|
+
elif nn == n2 or n2 is None and self._atoms[nn] == H:
|
|
815
|
+
t0 = 2
|
|
816
|
+
if nm == n1:
|
|
817
|
+
t1 = 1
|
|
818
|
+
elif nm == n3 or n3 is None and self._atoms[nm] == H:
|
|
819
|
+
t1 = 3
|
|
820
|
+
else:
|
|
821
|
+
raise KeyError
|
|
822
|
+
elif nn == n3 or n3 is None and self._atoms[nn] == H:
|
|
823
|
+
t0 = 3
|
|
824
|
+
if nm == n0:
|
|
825
|
+
t1 = 0
|
|
826
|
+
elif nm == n2 or n2 is None and self._atoms[nm] == H:
|
|
827
|
+
t1 = 2
|
|
828
|
+
else:
|
|
829
|
+
raise KeyError
|
|
830
|
+
else:
|
|
831
|
+
raise KeyError
|
|
832
|
+
|
|
833
|
+
if _alkene_translate[(t0, t1)]:
|
|
834
|
+
return not s
|
|
835
|
+
return s
|
|
836
|
+
|
|
837
|
+
@cached_property
|
|
838
|
+
def _wedge_map(self):
|
|
839
|
+
atoms = self._atoms
|
|
840
|
+
|
|
841
|
+
overlap = set()
|
|
842
|
+
space = []
|
|
843
|
+
solved = []
|
|
844
|
+
seen = set()
|
|
845
|
+
for n, env in self.stereogenic_allenes.items():
|
|
846
|
+
if atoms[n].stereo is None:
|
|
847
|
+
continue
|
|
848
|
+
term = self._stereo_allenes_terminals[n]
|
|
849
|
+
overlap.update(term) # don't allow incoming wedge to allenes terminals
|
|
850
|
+
orders = [(*env[:2], *term, n, True), (*env[1::-1], *term[::-1], n, True)]
|
|
851
|
+
if env[2]:
|
|
852
|
+
orders.append((env[2], env[1], *term, n, True))
|
|
853
|
+
if env[3]:
|
|
854
|
+
orders.append((env[3], env[0], *term[::-1], n, True))
|
|
855
|
+
space.append(orders)
|
|
856
|
+
for n, env in self.stereogenic_tetrahedrons.items():
|
|
857
|
+
if atoms[n].stereo is None:
|
|
858
|
+
continue
|
|
859
|
+
overlap.add(n) # don't allow incoming wedge to stereo tetrahedrons
|
|
860
|
+
order = list(env)
|
|
861
|
+
orders = [(*order, n, False)]
|
|
862
|
+
for _ in range(1, len(order)):
|
|
863
|
+
order = order.copy()
|
|
864
|
+
order.append(order.pop(0))
|
|
865
|
+
orders.append((*order, n, False))
|
|
866
|
+
space.append(orders)
|
|
867
|
+
|
|
868
|
+
while space:
|
|
869
|
+
ls = len(space)
|
|
870
|
+
unsolved = []
|
|
871
|
+
for orders in space:
|
|
872
|
+
good = []
|
|
873
|
+
if orders[0][-1]:
|
|
874
|
+
for x in orders:
|
|
875
|
+
x0 = x[0]
|
|
876
|
+
if x0 in seen or x0 not in overlap:
|
|
877
|
+
good.append(x)
|
|
878
|
+
seen.add(x[2])
|
|
879
|
+
if good:
|
|
880
|
+
solved.append(max(good, key=lambda x: (not atoms[x[0]].in_ring, atoms[x[0]].atomic_number)))
|
|
881
|
+
else:
|
|
882
|
+
unsolved.append(orders)
|
|
883
|
+
else:
|
|
884
|
+
for x in orders:
|
|
885
|
+
x0 = x[0]
|
|
886
|
+
if x0 in seen or x0 not in overlap:
|
|
887
|
+
good.append(x)
|
|
888
|
+
if good:
|
|
889
|
+
seen.add(x[-2])
|
|
890
|
+
solved.append(max(good, key=lambda x: (not atoms[x[0]].in_ring, atoms[x[0]].atomic_number)))
|
|
891
|
+
else:
|
|
892
|
+
unsolved.append(orders)
|
|
893
|
+
space = unsolved
|
|
894
|
+
if len(unsolved) == ls:
|
|
895
|
+
break
|
|
896
|
+
|
|
897
|
+
solved = [y for x in solved if (y := self.__wedge_sign(x))]
|
|
898
|
+
if not space:
|
|
899
|
+
return solved
|
|
900
|
+
|
|
901
|
+
for orders in product(*space):
|
|
902
|
+
used = set()
|
|
903
|
+
wedge = []
|
|
904
|
+
for order in orders:
|
|
905
|
+
if order[-1]: # allene
|
|
906
|
+
if (order[0], order[2]) in used:
|
|
907
|
+
break
|
|
908
|
+
used.add((order[2], order[0]))
|
|
909
|
+
wedge.append(self.__wedge_sign(order))
|
|
910
|
+
else: # TH
|
|
911
|
+
n = order[-2]
|
|
912
|
+
if (order[0], n) in used:
|
|
913
|
+
break
|
|
914
|
+
used.add((n, order[0]))
|
|
915
|
+
wedge.append(self.__wedge_sign(order))
|
|
916
|
+
else: # found
|
|
917
|
+
solved.extend(wedge)
|
|
918
|
+
return solved
|
|
919
|
+
logger.info('wedge stereo mapping failed')
|
|
920
|
+
return solved
|
|
921
|
+
|
|
922
|
+
def __wedge_sign(self, order):
|
|
923
|
+
if order[-1]: # allene
|
|
924
|
+
s = self._translate_allene_sign(order[-2], order[0], order[1])
|
|
925
|
+
v = _allene_sign(1, self._atoms[order[2]].xy, self._atoms[order[3]].xy, self._atoms[order[1]].xy)
|
|
926
|
+
if not v:
|
|
927
|
+
logger.info(f'need 2d clean. allenes wedge stereo ambiguous for atom {order[-2]}')
|
|
928
|
+
if s:
|
|
929
|
+
return order[2], order[0], v
|
|
930
|
+
else:
|
|
931
|
+
return order[2], order[0], -v
|
|
932
|
+
else: # TH
|
|
933
|
+
n = order[-2]
|
|
934
|
+
s = self._translate_tetrahedron_sign(n, order[:-2])
|
|
935
|
+
# need recalculation if XY changed
|
|
936
|
+
ao0 = self._atoms[order[0]]
|
|
937
|
+
ao1 = self._atoms[order[1]]
|
|
938
|
+
ao2 = self._atoms[order[2]]
|
|
939
|
+
if len(order) == 5:
|
|
940
|
+
an = self._atoms[n]
|
|
941
|
+
v = _pyramid_sign((an.x, an.y, 0),
|
|
942
|
+
(ao0.x, ao0.y, 1),
|
|
943
|
+
(ao1.x, ao1.y, 0),
|
|
944
|
+
(ao2.x, ao2.y, 0))
|
|
945
|
+
else:
|
|
946
|
+
ao3 = self._atoms[order[3]]
|
|
947
|
+
v = _pyramid_sign((ao3.x, ao3.y, 0),
|
|
948
|
+
(ao0.x, ao0.y, 1),
|
|
949
|
+
(ao1.x, ao1.y, 0),
|
|
950
|
+
(ao2.x, ao2.y, 0))
|
|
951
|
+
if not v:
|
|
952
|
+
logger.info(f'need 2d clean. tetrahedron wedge stereo ambiguous for atom {n}')
|
|
953
|
+
if s:
|
|
954
|
+
return n, order[0], v
|
|
955
|
+
else:
|
|
956
|
+
return n, order[0], -v
|
|
957
|
+
|
|
958
|
+
@cached_property
|
|
959
|
+
def _chiral_morgan(self) -> dict[int, int]:
|
|
960
|
+
stereo_atoms = {n for n, a in self.atoms() if a.stereo is not None}
|
|
961
|
+
stereo_bonds = {n for n, mb in self._bonds.items() if any(b.stereo is not None for m, b in mb.items())}
|
|
962
|
+
if not stereo_atoms and not stereo_bonds:
|
|
963
|
+
return self.atoms_order
|
|
964
|
+
|
|
965
|
+
morgan = self.atoms_order.copy()
|
|
966
|
+
atoms_stereo = stereo_atoms.intersection(self.tetrahedrons)
|
|
967
|
+
allenes_stereo = stereo_atoms - atoms_stereo
|
|
968
|
+
|
|
969
|
+
cis_trans_terminals = self._stereo_cis_trans_terminals
|
|
970
|
+
cis_trans_stereo = {cis_trans_terminals[n] for n in stereo_bonds}
|
|
971
|
+
|
|
972
|
+
while True:
|
|
973
|
+
# try iteratively differentiate stereo atoms.
|
|
974
|
+
morgan, atoms_stereo, cis_trans_stereo, allenes_stereo, atoms_groups, cis_trans_groups, allenes_groups = \
|
|
975
|
+
self.__differentiation(morgan, atoms_stereo, cis_trans_stereo, allenes_stereo)
|
|
976
|
+
if not atoms_groups and not cis_trans_groups and not allenes_groups:
|
|
977
|
+
break
|
|
978
|
+
# for some rings differentiation by morgan impossible. try randomly set new weights.
|
|
979
|
+
# sometimes this will lead to pseudo chiral centers and non-unique morgan.
|
|
980
|
+
for group in atoms_groups:
|
|
981
|
+
for n in group[:len(group) // 2]: # set new weight in half of group randomly.
|
|
982
|
+
morgan[n] = -morgan[n]
|
|
983
|
+
for group in cis_trans_groups:
|
|
984
|
+
for n, _ in group[:len(group) // 2]: # set new weight in half of group randomly.
|
|
985
|
+
morgan[n] = -morgan[n]
|
|
986
|
+
for group in allenes_groups:
|
|
987
|
+
for n in group[:len(group) // 2]: # set new weight in half of group randomly.
|
|
988
|
+
morgan[n] = -morgan[n]
|
|
989
|
+
morgan = _morgan(morgan, self.int_adjacency)
|
|
990
|
+
return morgan
|
|
991
|
+
|
|
992
|
+
@cached_property
|
|
993
|
+
def __chiral_centers(self):
|
|
994
|
+
atoms_rings = self.atoms_rings
|
|
995
|
+
tetrahedrons = self.stereogenic_tetrahedrons
|
|
996
|
+
cis_trans = self.stereogenic_cis_trans
|
|
997
|
+
allenes_centers = self._stereo_allenes_centers
|
|
998
|
+
cis_trans_terminals = self._stereo_cis_trans_terminals
|
|
999
|
+
cis_trans_centers = self._stereo_cis_trans_centers
|
|
1000
|
+
morgan = self._chiral_morgan
|
|
1001
|
+
|
|
1002
|
+
# find new chiral atoms and bonds.
|
|
1003
|
+
# tetrahedron is chiral if all its neighbors are unique.
|
|
1004
|
+
chiral_t = {n for n, env in tetrahedrons.items() if len({morgan[x] for x in env}) == len(env)}
|
|
1005
|
+
# tetrahedrons-linkers is chiral if in each rings neighbors are unique.
|
|
1006
|
+
chiral_t.update(n for n, (n1, n2, m1, m2) in self.rings_linker_tetrahedrons.items()
|
|
1007
|
+
if morgan[n1] != morgan[n2] and morgan[m1] != morgan[m2])
|
|
1008
|
+
|
|
1009
|
+
# required for axes detection.
|
|
1010
|
+
graph = {}
|
|
1011
|
+
stereogenic = set()
|
|
1012
|
+
pseudo = {}
|
|
1013
|
+
|
|
1014
|
+
# double bond is chiral if neighbors of each terminal atom is unique.
|
|
1015
|
+
# ring-linkers and rings-attached also takes into account.
|
|
1016
|
+
chiral_c = set()
|
|
1017
|
+
chiral_a = set()
|
|
1018
|
+
for path, (n1, m1, n2, m2) in self.stereogenic_cumulenes.items():
|
|
1019
|
+
if morgan[n1] != morgan.get(n2, 0) and morgan[m1] != morgan.get(m2, 0):
|
|
1020
|
+
n, m = path[0], path[-1]
|
|
1021
|
+
if len(path) % 2:
|
|
1022
|
+
chiral_a.add(path[len(path) // 2])
|
|
1023
|
+
else:
|
|
1024
|
+
chiral_c.add(n)
|
|
1025
|
+
stereogenic.add(n)
|
|
1026
|
+
stereogenic.add(m)
|
|
1027
|
+
# ring cumulenes always chiral. can be already added.
|
|
1028
|
+
for nm in self.ring_cumulenes_terminals:
|
|
1029
|
+
n, m = nm
|
|
1030
|
+
if any(len(x) < 8 for x in atoms_rings[n]): # skip small rings.
|
|
1031
|
+
if n in chiral_c: # remove already added small rings cumulenes.
|
|
1032
|
+
chiral_c.discard(n)
|
|
1033
|
+
if m in chiral_c:
|
|
1034
|
+
chiral_c.discard(m)
|
|
1035
|
+
elif n in allenes_centers and (c := allenes_centers[n]) in chiral_a:
|
|
1036
|
+
chiral_a.discard(c)
|
|
1037
|
+
continue
|
|
1038
|
+
elif nm in cis_trans:
|
|
1039
|
+
chiral_c.add(n)
|
|
1040
|
+
else:
|
|
1041
|
+
chiral_a.add(allenes_centers[n])
|
|
1042
|
+
pseudo[m] = n
|
|
1043
|
+
graph[n] = set()
|
|
1044
|
+
stereogenic.add(n)
|
|
1045
|
+
|
|
1046
|
+
# find chiral axes. build graph of stereogenic atoms in rings.
|
|
1047
|
+
# atoms connected then located in same ring or cumulene.
|
|
1048
|
+
for n, env in self.ring_tetrahedrons.items():
|
|
1049
|
+
if len(env) == 2: # one or zero non-ring neighbors stereogenic.
|
|
1050
|
+
n1, n2 = env
|
|
1051
|
+
if morgan[n1] == morgan[n2]: # only unique non-ring members required.
|
|
1052
|
+
continue
|
|
1053
|
+
graph[n] = set()
|
|
1054
|
+
stereogenic.add(n) # non-linker tetrahedrons in rings - stereogenic.
|
|
1055
|
+
for n, (n1, n2, m1, m2) in self.rings_linker_tetrahedrons.items():
|
|
1056
|
+
graph[n] = set()
|
|
1057
|
+
if morgan[n1] != morgan[n2] or morgan[m1] != morgan[m2]:
|
|
1058
|
+
stereogenic.add(n) # linkers with at least one unsymmetric ring.
|
|
1059
|
+
for n, m in self.rings_linker_cumulenes_terminals:
|
|
1060
|
+
graph[n] = {m}
|
|
1061
|
+
graph[m] = {n}
|
|
1062
|
+
# stereogenic atoms already found.
|
|
1063
|
+
for (n, m), env in self.ring_attached_cumulenes.items():
|
|
1064
|
+
if len(env) == 2:
|
|
1065
|
+
n1, n2 = env
|
|
1066
|
+
if morgan[n1] == morgan[n2]: # only unique non-ring members required.
|
|
1067
|
+
continue
|
|
1068
|
+
if n in atoms_rings:
|
|
1069
|
+
graph[n] = set() # non ring endpoints not required.
|
|
1070
|
+
stereogenic.add(n) # mark as stereogenic
|
|
1071
|
+
else:
|
|
1072
|
+
graph[m] = set()
|
|
1073
|
+
stereogenic.add(m)
|
|
1074
|
+
|
|
1075
|
+
if len(graph) > 1: # add bonds to graph. bonds connects atoms in same rings and terminal atoms of cumulenes.
|
|
1076
|
+
for n, ms in graph.items():
|
|
1077
|
+
for r in atoms_rings[n]:
|
|
1078
|
+
for m in r:
|
|
1079
|
+
if n == m:
|
|
1080
|
+
continue
|
|
1081
|
+
elif m in graph:
|
|
1082
|
+
ms.add(m)
|
|
1083
|
+
elif m in pseudo and (m := pseudo[m]) != n:
|
|
1084
|
+
ms.add(m)
|
|
1085
|
+
# remove not stereogenic terminals.
|
|
1086
|
+
while True:
|
|
1087
|
+
try:
|
|
1088
|
+
n = next(n for n, ms in graph.items() if not ms or len(ms) == 1 and n not in stereogenic)
|
|
1089
|
+
except StopIteration:
|
|
1090
|
+
break
|
|
1091
|
+
for m in graph.pop(n):
|
|
1092
|
+
graph[m].discard(n)
|
|
1093
|
+
# update chiral atoms.
|
|
1094
|
+
for n in graph:
|
|
1095
|
+
if n in tetrahedrons:
|
|
1096
|
+
chiral_t.add(n)
|
|
1097
|
+
elif n in allenes_centers:
|
|
1098
|
+
chiral_a.add(allenes_centers[n])
|
|
1099
|
+
else:
|
|
1100
|
+
chiral_c.add(n)
|
|
1101
|
+
|
|
1102
|
+
# skip already marked.
|
|
1103
|
+
stereo_atoms = {n for n, a in self.atoms() if a.stereo is not None}
|
|
1104
|
+
chiral_t.difference_update(stereo_atoms)
|
|
1105
|
+
chiral_a.difference_update(stereo_atoms)
|
|
1106
|
+
diff = set()
|
|
1107
|
+
for n in chiral_c:
|
|
1108
|
+
i, j = cis_trans_centers[n]
|
|
1109
|
+
if self._bonds[i][j].stereo is None:
|
|
1110
|
+
diff.add(cis_trans_terminals[n])
|
|
1111
|
+
return chiral_t, diff, chiral_a
|
|
1112
|
+
|
|
1113
|
+
def __differentiation(self, morgan, atoms_stereo, cis_trans_stereo, allenes_stereo):
|
|
1114
|
+
bonds = self.int_adjacency
|
|
1115
|
+
|
|
1116
|
+
tetrahedrons = self.stereogenic_tetrahedrons
|
|
1117
|
+
cis_trans = self.stereogenic_cis_trans
|
|
1118
|
+
allenes = self.stereogenic_allenes
|
|
1119
|
+
|
|
1120
|
+
translate_tetrahedron = self._translate_tetrahedron_sign
|
|
1121
|
+
translate_cis_trans = self._translate_cis_trans_sign
|
|
1122
|
+
translate_allene = self._translate_allene_sign
|
|
1123
|
+
|
|
1124
|
+
while True:
|
|
1125
|
+
morgan_update = {}
|
|
1126
|
+
atoms_groups = []
|
|
1127
|
+
cis_trans_groups = []
|
|
1128
|
+
allenes_groups = []
|
|
1129
|
+
# recalculate morgan weights with taking into account existing stereo marks.
|
|
1130
|
+
if atoms_stereo:
|
|
1131
|
+
grouped_stereo = defaultdict(list)
|
|
1132
|
+
for n in atoms_stereo:
|
|
1133
|
+
grouped_stereo[morgan[n]].append(n) # collect equal stereo atoms.
|
|
1134
|
+
for group in grouped_stereo.values():
|
|
1135
|
+
if not len(group) % 2: # only even number of equal stereo atoms give new stereo center.
|
|
1136
|
+
# process only truly stereogenic.
|
|
1137
|
+
if len(env := tetrahedrons[group[0]]) == len({morgan[x] for x in env}):
|
|
1138
|
+
s = [n for n in group if translate_tetrahedron(n, sorted(tetrahedrons[n], key=morgan.get))]
|
|
1139
|
+
if 0 < len(s) < len(group): # RS pair required.
|
|
1140
|
+
for m in s:
|
|
1141
|
+
morgan_update[m] = -morgan[m]
|
|
1142
|
+
for n in group: # prevent checks repeating.
|
|
1143
|
+
atoms_stereo.discard(n)
|
|
1144
|
+
else: # stereo group in rings. unambiguous environment order impossible.
|
|
1145
|
+
atoms_groups.append(group)
|
|
1146
|
+
|
|
1147
|
+
if cis_trans_stereo:
|
|
1148
|
+
grouped_stereo = defaultdict(list)
|
|
1149
|
+
for nm in cis_trans_stereo:
|
|
1150
|
+
n, m = nm
|
|
1151
|
+
if (mn := morgan[n]) <= (mm := morgan[m]):
|
|
1152
|
+
grouped_stereo[mn].append((n, nm))
|
|
1153
|
+
else:
|
|
1154
|
+
grouped_stereo[mm].append((m, nm))
|
|
1155
|
+
for group in grouped_stereo.values():
|
|
1156
|
+
if not len(group) % 2: # only even number of equal stereo bonds give new stereo center.
|
|
1157
|
+
n1, m1, n2, m2 = cis_trans[group[0][1]]
|
|
1158
|
+
if morgan[n1] != morgan.get(n2, 0) and morgan[m1] != morgan.get(m2, 0):
|
|
1159
|
+
s = []
|
|
1160
|
+
for x, nm in group:
|
|
1161
|
+
n, m = nm
|
|
1162
|
+
n1, m1, n2, m2 = cis_trans[nm]
|
|
1163
|
+
if n2 is None:
|
|
1164
|
+
a = n1
|
|
1165
|
+
else:
|
|
1166
|
+
a = min(n1, n2, key=morgan.get)
|
|
1167
|
+
if m2 is None:
|
|
1168
|
+
b = m1
|
|
1169
|
+
else:
|
|
1170
|
+
b = min(m1, m2, key=morgan.get)
|
|
1171
|
+
if translate_cis_trans(n, m, a, b):
|
|
1172
|
+
s.append(x)
|
|
1173
|
+
if 0 < len(s) < len(group): # RS pair required.
|
|
1174
|
+
for n in s:
|
|
1175
|
+
morgan_update[n] = -morgan[n]
|
|
1176
|
+
for _, nm in group:
|
|
1177
|
+
cis_trans_stereo.discard(nm)
|
|
1178
|
+
else:
|
|
1179
|
+
cis_trans_groups.append(group)
|
|
1180
|
+
|
|
1181
|
+
if allenes_stereo:
|
|
1182
|
+
grouped_stereo = defaultdict(list)
|
|
1183
|
+
for c in allenes_stereo:
|
|
1184
|
+
grouped_stereo[morgan[c]].append(c)
|
|
1185
|
+
for group in grouped_stereo.values():
|
|
1186
|
+
if not len(group) % 2: # only even number of equal stereo bonds give new stereo center.
|
|
1187
|
+
n1, m1, n2, m2 = allenes[group[0]]
|
|
1188
|
+
if morgan[n1] != morgan.get(n2, 0) and morgan[m1] != morgan.get(m2, 0):
|
|
1189
|
+
s = []
|
|
1190
|
+
for c in group:
|
|
1191
|
+
n1, m1, n2, m2 = allenes[c]
|
|
1192
|
+
if n2 is None:
|
|
1193
|
+
a = n1
|
|
1194
|
+
else:
|
|
1195
|
+
a = min(n1, n2, key=morgan.get)
|
|
1196
|
+
if m2 is None:
|
|
1197
|
+
b = m1
|
|
1198
|
+
else:
|
|
1199
|
+
b = min(m1, m2, key=morgan.get)
|
|
1200
|
+
if translate_allene(c, a, b):
|
|
1201
|
+
s.append(c)
|
|
1202
|
+
if 0 < len(s) < len(group): # RS pair required.
|
|
1203
|
+
for c in s:
|
|
1204
|
+
morgan_update[c] = -morgan[c]
|
|
1205
|
+
for c in group:
|
|
1206
|
+
allenes_stereo.discard(c)
|
|
1207
|
+
else:
|
|
1208
|
+
allenes_groups.append(group)
|
|
1209
|
+
if not morgan_update:
|
|
1210
|
+
break
|
|
1211
|
+
morgan = _morgan({**morgan, **morgan_update}, bonds)
|
|
1212
|
+
return morgan, atoms_stereo, cis_trans_stereo, allenes_stereo, atoms_groups, cis_trans_groups, allenes_groups
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
__all__ = ['MoleculeStereo']
|