chython 2.4__cp312-cp312-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chython/__init__.py +31 -0
- chython/_functions.py +69 -0
- chython/algorithms/__init__.py +18 -0
- chython/algorithms/_isomorphism.cp312-win_amd64.pyd +0 -0
- chython/algorithms/_isomorphism.pyx +189 -0
- chython/algorithms/aromatics/__init__.py +27 -0
- chython/algorithms/aromatics/_rules.py +124 -0
- chython/algorithms/aromatics/kekule.py +530 -0
- chython/algorithms/aromatics/test/__init__.py +18 -0
- chython/algorithms/aromatics/test/test_kekule.py +160 -0
- chython/algorithms/aromatics/test/test_thiele.py +263 -0
- chython/algorithms/aromatics/thiele.py +226 -0
- chython/algorithms/calculate2d/__init__.py +23 -0
- chython/algorithms/calculate2d/clean2d.js +1 -0
- chython/algorithms/calculate2d/molecule.py +149 -0
- chython/algorithms/calculate2d/reaction.py +80 -0
- chython/algorithms/depict.py +523 -0
- chython/algorithms/fingerprints/__init__.py +48 -0
- chython/algorithms/fingerprints/linear.py +208 -0
- chython/algorithms/fingerprints/morgan.py +137 -0
- chython/algorithms/fingerprints/test/__init__.py +18 -0
- chython/algorithms/fingerprints/test/test_linear.py +127 -0
- chython/algorithms/fingerprints/test/test_morgan.py +79 -0
- chython/algorithms/isomorphism.py +667 -0
- chython/algorithms/mapping/__init__.py +28 -0
- chython/algorithms/mapping/_groups.py +94 -0
- chython/algorithms/mapping/_reactions.py +72 -0
- chython/algorithms/mapping/attention.py +216 -0
- chython/algorithms/mapping/fixmapper.py +84 -0
- chython/algorithms/mapping/groups.py +137 -0
- chython/algorithms/mcs.py +202 -0
- chython/algorithms/morgan.py +82 -0
- chython/algorithms/rings.py +551 -0
- chython/algorithms/smiles.py +556 -0
- chython/algorithms/standardize/__init__.py +30 -0
- chython/algorithms/standardize/_charged.py +115 -0
- chython/algorithms/standardize/_groups.py +926 -0
- chython/algorithms/standardize/_metal_organics.py +189 -0
- chython/algorithms/standardize/_reagents.py +44 -0
- chython/algorithms/standardize/_salts.py +68 -0
- chython/algorithms/standardize/molecule.py +479 -0
- chython/algorithms/standardize/reaction.py +429 -0
- chython/algorithms/standardize/resonance.py +196 -0
- chython/algorithms/standardize/salts.py +128 -0
- chython/algorithms/standardize/saturation.py +378 -0
- chython/algorithms/standardize/test/__init__.py +18 -0
- chython/algorithms/standardize/test/test_groups.py +118 -0
- chython/algorithms/stereo.py +1222 -0
- chython/algorithms/tautomers/__init__.py +171 -0
- chython/algorithms/tautomers/_acid.py +59 -0
- chython/algorithms/tautomers/_base.py +116 -0
- chython/algorithms/tautomers/_keto_enol.py +107 -0
- chython/algorithms/tautomers/acid_base.py +214 -0
- chython/algorithms/tautomers/heteroarenes.py +108 -0
- chython/algorithms/tautomers/keto_enol.py +148 -0
- chython/algorithms/tautomers/test/__init__.py +18 -0
- chython/algorithms/tautomers/test/test_tautomers.py +80 -0
- chython/algorithms/test/__init__.py +18 -0
- chython/algorithms/test/test_isomorphism.py +58 -0
- chython/algorithms/test/test_smiles.py +76 -0
- chython/algorithms/x3dom.py +355 -0
- chython/containers/__init__.py +43 -0
- chython/containers/_pack_v2.cp312-win_amd64.pyd +0 -0
- chython/containers/_pack_v2.pyx +271 -0
- chython/containers/_unpack_v0v2.cp312-win_amd64.pyd +0 -0
- chython/containers/_unpack_v0v2.pyx +300 -0
- chython/containers/bonds.py +255 -0
- chython/containers/cgr.py +115 -0
- chython/containers/graph.py +200 -0
- chython/containers/molecule.py +695 -0
- chython/containers/query.py +66 -0
- chython/containers/reaction.py +324 -0
- chython/exceptions.py +114 -0
- chython/files/MRVrw.py +523 -0
- chython/files/PDBrw.py +257 -0
- chython/files/RDFrw.py +298 -0
- chython/files/SDFrw.py +221 -0
- chython/files/__init__.py +29 -0
- chython/files/_convert.py +207 -0
- chython/files/_mapping.py +117 -0
- chython/files/_xyz.cp312-win_amd64.pyd +0 -0
- chython/files/_xyz.pyx +74 -0
- chython/files/daylight/__init__.py +24 -0
- chython/files/daylight/parser.py +157 -0
- chython/files/daylight/smarts.py +107 -0
- chython/files/daylight/smiles.py +245 -0
- chython/files/daylight/test/__init__.py +18 -0
- chython/files/daylight/test/test_daylight_smarts.py +289 -0
- chython/files/daylight/test/test_daylight_smiles.py +101 -0
- chython/files/daylight/test/test_parser.py +118 -0
- chython/files/daylight/test/test_tokenize.py +77 -0
- chython/files/daylight/tokenize.py +376 -0
- chython/files/libinchi/__init__.py +22 -0
- chython/files/libinchi/libinchi.dll +0 -0
- chython/files/libinchi/wrapper.py +555 -0
- chython/files/mdl/__init__.py +25 -0
- chython/files/mdl/emol.py +209 -0
- chython/files/mdl/erxn.py +67 -0
- chython/files/mdl/mol.py +144 -0
- chython/files/mdl/read.py +225 -0
- chython/files/mdl/rxn.py +67 -0
- chython/files/mdl/stereo.py +63 -0
- chython/files/mdl/write.py +165 -0
- chython/files/xyz.py +78 -0
- chython/periodictable/__init__.py +69 -0
- chython/periodictable/base/__init__.py +25 -0
- chython/periodictable/base/dynamic.py +149 -0
- chython/periodictable/base/element.py +452 -0
- chython/periodictable/base/groups.py +90 -0
- chython/periodictable/base/periods.py +46 -0
- chython/periodictable/base/query.py +473 -0
- chython/periodictable/base/vector.py +119 -0
- chython/periodictable/groupI.py +252 -0
- chython/periodictable/groupII.py +223 -0
- chython/periodictable/groupIII.py +1154 -0
- chython/periodictable/groupIV.py +210 -0
- chython/periodictable/groupIX.py +196 -0
- chython/periodictable/groupV.py +192 -0
- chython/periodictable/groupVI.py +183 -0
- chython/periodictable/groupVII.py +162 -0
- chython/periodictable/groupVIII.py +161 -0
- chython/periodictable/groupX.py +166 -0
- chython/periodictable/groupXI.py +164 -0
- chython/periodictable/groupXII.py +158 -0
- chython/periodictable/groupXIII.py +239 -0
- chython/periodictable/groupXIV.py +262 -0
- chython/periodictable/groupXV.py +276 -0
- chython/periodictable/groupXVI.py +457 -0
- chython/periodictable/groupXVII.py +324 -0
- chython/periodictable/groupXVIII.py +260 -0
- chython/reactor/__init__.py +24 -0
- chython/reactor/base.py +212 -0
- chython/reactor/deprotection.py +565 -0
- chython/reactor/groups.py +128 -0
- chython/reactor/reactions/__init__.py +157 -0
- chython/reactor/reactions/_amidation.py +58 -0
- chython/reactor/reactions/_amine_isocyanate.py +52 -0
- chython/reactor/reactions/_buchwald_hartwig.py +50 -0
- chython/reactor/reactions/_esterification.py +49 -0
- chython/reactor/reactions/_macmillan.py +43 -0
- chython/reactor/reactions/_reductive_amination.py +50 -0
- chython/reactor/reactions/_sonogashira.py +49 -0
- chython/reactor/reactions/_sulfonamidation.py +52 -0
- chython/reactor/reactions/_suzuki_miyaura.py +88 -0
- chython/reactor/reactions/ufe.py +99 -0
- chython/reactor/reactor.py +170 -0
- chython/reactor/retro/__init__.py +77 -0
- chython/reactor/retro/_amidation.py +39 -0
- chython/reactor/retro/_aryl_amination.py +32 -0
- chython/reactor/retro/_mitsunobu.py +49 -0
- chython/reactor/retro/_sonogashira.py +49 -0
- chython/reactor/retro/_suzuki_miyaura.py +54 -0
- chython/reactor/scaffold.py +122 -0
- chython/reactor/test/__init__.py +18 -0
- chython/reactor/test/test_deprotection.py +57 -0
- chython/reactor/test/test_reactor.py +40 -0
- chython/reactor/test/test_scaffold.py +57 -0
- chython/reactor/test/test_transformer.py +40 -0
- chython/reactor/transformer.py +62 -0
- chython/utils/__init__.py +57 -0
- chython/utils/free_wilson.py +114 -0
- chython/utils/grid.py +130 -0
- chython/utils/rdkit.py +165 -0
- chython/utils/retro.py +139 -0
- chython/utils/svg.py +57 -0
- chython/utils/test/__init__.py +18 -0
- chython/utils/test/test_rdkit.py +71 -0
- chython-2.4.dist-info/LICENSE +165 -0
- chython-2.4.dist-info/METADATA +101 -0
- chython-2.4.dist-info/RECORD +171 -0
- chython-2.4.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2022, 2023 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 ...exceptions import IncorrectSmiles
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parser(tokens, strong_cycle):
|
|
24
|
+
log = []
|
|
25
|
+
t1 = tokens[0][0]
|
|
26
|
+
if t1 == 2:
|
|
27
|
+
if tokens[1][0] not in (0, 8):
|
|
28
|
+
raise IncorrectSmiles('not atom started')
|
|
29
|
+
elif t1 not in (0, 8):
|
|
30
|
+
raise IncorrectSmiles('not atom started')
|
|
31
|
+
|
|
32
|
+
atoms = []
|
|
33
|
+
bonds = []
|
|
34
|
+
order = defaultdict(list)
|
|
35
|
+
atoms_types = []
|
|
36
|
+
atom_num = 0
|
|
37
|
+
last_num = 0
|
|
38
|
+
stack = []
|
|
39
|
+
cycles = {}
|
|
40
|
+
stereo_atoms = {}
|
|
41
|
+
stereo_bonds = defaultdict(dict)
|
|
42
|
+
previous = None
|
|
43
|
+
|
|
44
|
+
for token_type, token in tokens:
|
|
45
|
+
if token_type == 2: # ((((((
|
|
46
|
+
if previous:
|
|
47
|
+
if previous[0] != 4:
|
|
48
|
+
raise IncorrectSmiles('bond before side chain')
|
|
49
|
+
previous = None
|
|
50
|
+
stack.append(last_num)
|
|
51
|
+
elif token_type == 3: # ))))))
|
|
52
|
+
if previous:
|
|
53
|
+
raise IncorrectSmiles('bond before closure')
|
|
54
|
+
try:
|
|
55
|
+
last_num = stack.pop()
|
|
56
|
+
except IndexError:
|
|
57
|
+
raise IncorrectSmiles('close chain more than open')
|
|
58
|
+
elif token_type in (1, 4, 9, 10, 12): # bonds. only keeping for atoms connecting
|
|
59
|
+
if previous:
|
|
60
|
+
raise IncorrectSmiles('2 bonds in a row')
|
|
61
|
+
elif not atoms:
|
|
62
|
+
raise IncorrectSmiles('started from bond')
|
|
63
|
+
previous = (token_type, token)
|
|
64
|
+
elif token_type == 6: # cycle
|
|
65
|
+
if previous and previous[0] == 4:
|
|
66
|
+
raise IncorrectSmiles('dot-cycle pattern invalid')
|
|
67
|
+
elif token not in cycles:
|
|
68
|
+
cycles[token] = (last_num, previous, len(order[last_num]))
|
|
69
|
+
order[last_num].append(None) # Reserve a table
|
|
70
|
+
previous = None
|
|
71
|
+
else:
|
|
72
|
+
a, ob, ind = cycles[token]
|
|
73
|
+
if ob:
|
|
74
|
+
if not previous:
|
|
75
|
+
bt, b = ob
|
|
76
|
+
if bt == 9: # closure open is \/ bonded
|
|
77
|
+
stereo_bonds[a][last_num] = b
|
|
78
|
+
b = 1
|
|
79
|
+
elif strong_cycle:
|
|
80
|
+
raise IncorrectSmiles('not equal cycle bonds')
|
|
81
|
+
else:
|
|
82
|
+
log.append('ignored difference in cycle bonds')
|
|
83
|
+
else:
|
|
84
|
+
bt, b = previous
|
|
85
|
+
obt, ob = ob
|
|
86
|
+
if bt == 9: # \/ bonds can be unequal
|
|
87
|
+
if obt == 9:
|
|
88
|
+
stereo_bonds[a][last_num] = ob
|
|
89
|
+
elif ob != 1:
|
|
90
|
+
raise IncorrectSmiles('not equal cycle bonds')
|
|
91
|
+
stereo_bonds[last_num][a] = b
|
|
92
|
+
b = 1
|
|
93
|
+
elif obt == 9:
|
|
94
|
+
if b != 1:
|
|
95
|
+
raise IncorrectSmiles('not equal cycle bonds')
|
|
96
|
+
stereo_bonds[a][last_num] = ob
|
|
97
|
+
elif b != ob:
|
|
98
|
+
raise IncorrectSmiles('not equal cycle bonds')
|
|
99
|
+
previous = None
|
|
100
|
+
elif previous:
|
|
101
|
+
bt, b = previous
|
|
102
|
+
if bt == 9: # stereo \/
|
|
103
|
+
stereo_bonds[last_num][a] = b
|
|
104
|
+
b = 1
|
|
105
|
+
elif strong_cycle:
|
|
106
|
+
raise IncorrectSmiles('not equal cycle bonds')
|
|
107
|
+
else:
|
|
108
|
+
log.append('ignored difference in cycle bonds')
|
|
109
|
+
previous = None
|
|
110
|
+
else:
|
|
111
|
+
b = 4 if atoms_types[last_num] == atoms_types[a] == 8 else 1
|
|
112
|
+
|
|
113
|
+
bonds.append((last_num, a, b))
|
|
114
|
+
order[a][ind] = last_num
|
|
115
|
+
order[last_num].append(a)
|
|
116
|
+
del cycles[token]
|
|
117
|
+
else: # atom
|
|
118
|
+
if atoms:
|
|
119
|
+
if not previous:
|
|
120
|
+
bonds.append((atom_num, last_num, 4 if token_type == atoms_types[last_num] == 8 else 1))
|
|
121
|
+
order[last_num].append(atom_num)
|
|
122
|
+
order[atom_num].append(last_num)
|
|
123
|
+
else:
|
|
124
|
+
bt, b = previous
|
|
125
|
+
if bt == 9:
|
|
126
|
+
bonds.append((atom_num, last_num, 4 if token_type == atoms_types[last_num] == 8 else 1))
|
|
127
|
+
order[last_num].append(atom_num)
|
|
128
|
+
order[atom_num].append(last_num)
|
|
129
|
+
|
|
130
|
+
stereo_bonds[last_num][atom_num] = b
|
|
131
|
+
stereo_bonds[atom_num][last_num] = not b
|
|
132
|
+
elif bt in (1, 10, 12):
|
|
133
|
+
bonds.append((atom_num, last_num, b))
|
|
134
|
+
order[last_num].append(atom_num)
|
|
135
|
+
order[atom_num].append(last_num)
|
|
136
|
+
# else bt == 4 - skip dot
|
|
137
|
+
previous = None
|
|
138
|
+
|
|
139
|
+
if token.get('stereo') is not None:
|
|
140
|
+
stereo_atoms[atom_num] = token.pop('stereo')
|
|
141
|
+
atoms.append(token)
|
|
142
|
+
atoms_types.append(token_type)
|
|
143
|
+
last_num = atom_num
|
|
144
|
+
atom_num += 1
|
|
145
|
+
|
|
146
|
+
if stack:
|
|
147
|
+
raise IncorrectSmiles('number of ( does not equal to number of )')
|
|
148
|
+
elif cycles:
|
|
149
|
+
raise IncorrectSmiles('cycle is not finished')
|
|
150
|
+
elif previous:
|
|
151
|
+
raise IncorrectSmiles('bond on the end')
|
|
152
|
+
|
|
153
|
+
return {'atoms': atoms, 'bonds': bonds, 'order': order, 'stereo_atoms': stereo_atoms,
|
|
154
|
+
'stereo_bonds': stereo_bonds, 'log': log}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
__all__ = ['parser']
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2022-2025 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# This file is part of chython.
|
|
5
|
+
#
|
|
6
|
+
# chython is free software; you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
8
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU Lesser General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
17
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
#
|
|
19
|
+
from functools import partial
|
|
20
|
+
from itertools import count
|
|
21
|
+
from re import compile, findall, search
|
|
22
|
+
from .parser import parser
|
|
23
|
+
from .tokenize import smarts_tokenize
|
|
24
|
+
from ...containers import QueryContainer, QueryBond
|
|
25
|
+
from ...periodictable import ListElement, QueryElement
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
cx_radicals = compile(r'\^[1-7]:[0-9]+(?:,[0-9]+)*')
|
|
29
|
+
|
|
30
|
+
# AD-HOC for masked atoms in SMARTS
|
|
31
|
+
# not thread safe
|
|
32
|
+
global_free_masked = count(10 ** 9 + 1)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def smarts(data: str):
|
|
36
|
+
"""
|
|
37
|
+
Parse SMARTS string.
|
|
38
|
+
|
|
39
|
+
* only D, a, h, r and !R atom primitives supported.
|
|
40
|
+
* bond order list (max 2) and not bond supported.
|
|
41
|
+
* [not]ring bond supported only in combination with explicit bonds, not bonds and bonds orders lists.
|
|
42
|
+
* mapping, charge and isotopes supported.
|
|
43
|
+
* list of elements supported.
|
|
44
|
+
* A - treats as any element. <A> primitive (aliphatic) ignored.
|
|
45
|
+
* M - treats as any metal
|
|
46
|
+
* <&> logic operator unsupported.
|
|
47
|
+
* <;> logic operator is mandatory except (however preferable) for charge, isotope, stereo marks.
|
|
48
|
+
* CXSMARTS radicals supported.
|
|
49
|
+
* masked atom - `chython.Reactor` specific mark for masking reactant atoms from deletion.
|
|
50
|
+
|
|
51
|
+
For example::
|
|
52
|
+
|
|
53
|
+
[C;r5,r6;a]-;!@[C;h1,h2;z2,z4] |^1:1| - aromatic C member of 5 or 6 atoms ring
|
|
54
|
+
connected with non-ring single bond to aromatic or SP2 radical C with 1 or 2 hydrogens.
|
|
55
|
+
|
|
56
|
+
* primitive <xN> - heteroatoms (e.g. x2 - two heteroatoms)
|
|
57
|
+
* primitive <zN> - hybridization (N = 1 - sp3, 2 - sp2, 3 - sp, 4 - aromatic)
|
|
58
|
+
* primitive <M> - masked atom
|
|
59
|
+
|
|
60
|
+
Note: atom numbers greater than 10 ** 9 forbidden for usage and reserved for masked atoms numbering.
|
|
61
|
+
In multiprocess mode has potential bugs in reaction enumeration task then used templates prepared from components
|
|
62
|
+
from different processes. For avoiding, prepare templates on single process and then share it.
|
|
63
|
+
"""
|
|
64
|
+
if not isinstance(data, str):
|
|
65
|
+
raise TypeError('Must be a SMARTS string')
|
|
66
|
+
smr, *cx = data.split()
|
|
67
|
+
|
|
68
|
+
parsed = parser(smarts_tokenize(smr), False)
|
|
69
|
+
|
|
70
|
+
if cx and cx[0].startswith('|') and cx[0].endswith('|'):
|
|
71
|
+
for x in findall(cx_radicals, cx[0]):
|
|
72
|
+
for i in x[3:].split(','):
|
|
73
|
+
parsed['atoms'][int(i)]['is_radical'] = True
|
|
74
|
+
|
|
75
|
+
for i, s in parsed['stereo_atoms'].items():
|
|
76
|
+
parsed['atoms'][i]['stereo'] = s
|
|
77
|
+
stereo_bonds = parsed['stereo_bonds']
|
|
78
|
+
|
|
79
|
+
g = QueryContainer(data)
|
|
80
|
+
|
|
81
|
+
mapping = {}
|
|
82
|
+
free = count(max(a.get('parsed_mapping', 0) for a in parsed['atoms']) + 1)
|
|
83
|
+
for i, a in enumerate(parsed['atoms']):
|
|
84
|
+
mapping[i] = n = a.pop('parsed_mapping', 0) or next(global_free_masked if a.get('masked') else free)
|
|
85
|
+
e = a.pop('element')
|
|
86
|
+
if isinstance(e, int):
|
|
87
|
+
e = QueryElement.from_atomic_number(e)
|
|
88
|
+
elif isinstance(e, str):
|
|
89
|
+
e = QueryElement.from_symbol(e)
|
|
90
|
+
else:
|
|
91
|
+
e = partial(ListElement, e)
|
|
92
|
+
g.add_atom(e(**a), n)
|
|
93
|
+
|
|
94
|
+
for n, m, b in parsed['bonds']:
|
|
95
|
+
if n in stereo_bonds and m in stereo_bonds:
|
|
96
|
+
if m not in stereo_bonds[n]: # only simple cis-trans supported, not cumulenes
|
|
97
|
+
_, s1 = stereo_bonds[n].popitem()
|
|
98
|
+
_, s2 = stereo_bonds[m].popitem()
|
|
99
|
+
if isinstance(b, int):
|
|
100
|
+
b = QueryBond(b, stereo=s1 == s2)
|
|
101
|
+
else:
|
|
102
|
+
b.stereo = s1 == s2
|
|
103
|
+
g.add_bond(mapping[n], mapping[m], b)
|
|
104
|
+
return g
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = ['smarts']
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2022-2024 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# This file is part of chython.
|
|
5
|
+
#
|
|
6
|
+
# chython is free software; you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
8
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU Lesser General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
17
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
#
|
|
19
|
+
from itertools import chain
|
|
20
|
+
from re import compile, findall, search
|
|
21
|
+
from typing import List, Union, Optional
|
|
22
|
+
from .parser import parser
|
|
23
|
+
from .tokenize import smiles_tokenize
|
|
24
|
+
from .._convert import create_molecule, create_reaction
|
|
25
|
+
from .._mapping import postprocess_parsed_molecule, postprocess_parsed_reaction
|
|
26
|
+
from ...containers import MoleculeContainer, ReactionContainer
|
|
27
|
+
from ...exceptions import IsChiral, NotChiral, ValenceError
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
cx_fragments = compile(r'f:(?:[0-9]+(?:\.[0-9]+)+)(?:,(?:[0-9]+(?:\.[0-9]+)+))*')
|
|
31
|
+
cx_radicals = compile(r'\^[1-7]:[0-9]+(?:,[0-9]+)*')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def smiles(data, /, *, ignore: bool = True, remap: bool = False, ignore_stereo: bool = False,
|
|
35
|
+
ignore_bad_isotopes: bool = False, keep_implicit: bool = False, ignore_carbon_radicals: bool = False,
|
|
36
|
+
ignore_aromatic_radicals: bool = True,
|
|
37
|
+
_r_cls=ReactionContainer, _m_cls=MoleculeContainer) -> Union[MoleculeContainer, ReactionContainer]:
|
|
38
|
+
"""
|
|
39
|
+
SMILES string parser
|
|
40
|
+
|
|
41
|
+
:param ignore: Skip some checks of data or try to fix some errors.
|
|
42
|
+
:param remap: Remap atom numbers started from one.
|
|
43
|
+
:param ignore_stereo: Ignore stereo data.
|
|
44
|
+
:param keep_implicit: keep given in smiles implicit hydrogen count, otherwise ignore on valence error.
|
|
45
|
+
:param ignore_bad_isotopes: reset invalid isotope mark to non-isotopic.
|
|
46
|
+
:param ignore_carbon_radicals: fill carbon radicals with hydrogen (X[C](X)X case).
|
|
47
|
+
:param ignore_aromatic_radicals: don't treat aromatic tokens like c[c]c as radicals.
|
|
48
|
+
"""
|
|
49
|
+
if not isinstance(data, str):
|
|
50
|
+
raise TypeError('Must be a SMILES string')
|
|
51
|
+
elif not data:
|
|
52
|
+
raise ValueError('Empty string')
|
|
53
|
+
|
|
54
|
+
contract: Optional[List[List[int]]] # typing
|
|
55
|
+
log = []
|
|
56
|
+
smi, *data = data.split()
|
|
57
|
+
if data and (cxs := data[0]).startswith('|') and cxs.endswith('|'):
|
|
58
|
+
fr = search(cx_fragments, cxs)
|
|
59
|
+
if fr is not None:
|
|
60
|
+
contract = [sorted(int(x) for x in x.split('.')) for x in fr.group()[2:].split(',')]
|
|
61
|
+
if len({x for x in contract for x in x}) < len([x for x in contract for x in x]):
|
|
62
|
+
log.append(f'collisions in cxsmiles fragments description: {cxs}')
|
|
63
|
+
contract = None
|
|
64
|
+
|
|
65
|
+
radicals = [int(x) for x in findall(cx_radicals, cxs) for x in x[3:].split(',')]
|
|
66
|
+
if radicals and len(set(radicals)) != len(radicals):
|
|
67
|
+
log.append(f'collisions in cxsmiles radicals description: {cxs}')
|
|
68
|
+
radicals = []
|
|
69
|
+
else:
|
|
70
|
+
radicals = [int(x) for x in findall(cx_radicals, cxs) for x in x[3:].split(',')]
|
|
71
|
+
if radicals:
|
|
72
|
+
if len(set(radicals)) != len(radicals):
|
|
73
|
+
log.append(f'collisions in cxsmiles radicals description: {cxs}')
|
|
74
|
+
radicals = []
|
|
75
|
+
contract = None
|
|
76
|
+
else:
|
|
77
|
+
radicals = []
|
|
78
|
+
contract = None
|
|
79
|
+
|
|
80
|
+
if '>' in smi:
|
|
81
|
+
record = {'reactants': [], 'reagents': [], 'products': [], 'log': log}
|
|
82
|
+
try:
|
|
83
|
+
reactants, reagents, products = smi.split('>')
|
|
84
|
+
except ValueError as e:
|
|
85
|
+
raise ValueError('invalid reaction smiles') from e
|
|
86
|
+
|
|
87
|
+
mol_count = 0
|
|
88
|
+
for k, d in zip(('reactants', 'products', 'reagents'), (reactants, products, reagents)):
|
|
89
|
+
if not d:
|
|
90
|
+
continue
|
|
91
|
+
for x in d.split('.'):
|
|
92
|
+
if not x:
|
|
93
|
+
if ignore:
|
|
94
|
+
log.append('two dots in line ignored')
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError('invalid reaction smiles. two dots in line')
|
|
97
|
+
else:
|
|
98
|
+
record[k].append(x)
|
|
99
|
+
mol_count += 1
|
|
100
|
+
|
|
101
|
+
if contract:
|
|
102
|
+
if max(x for x in contract for x in x) >= mol_count:
|
|
103
|
+
log.append(f'skipped invalid contract data: {contract}')
|
|
104
|
+
lr = len(record['reactants'])
|
|
105
|
+
lp = len(record['products'])
|
|
106
|
+
reactants = set(range(lr))
|
|
107
|
+
reagents = set(range(lr, mol_count - lp))
|
|
108
|
+
products = set(range(mol_count - lp, mol_count))
|
|
109
|
+
new_molecules: List[Optional[str]] = [None] * mol_count
|
|
110
|
+
for c in contract:
|
|
111
|
+
if reactants.issuperset(c):
|
|
112
|
+
new_molecules[c[0]] = '.'.join(record['reactants'][x] for x in c)
|
|
113
|
+
reactants.difference_update(c)
|
|
114
|
+
elif products.issuperset(c):
|
|
115
|
+
new_molecules[c[0]] = '.'.join(record['products'][x - mol_count] for x in c)
|
|
116
|
+
products.difference_update(c)
|
|
117
|
+
elif reagents.issuperset(c):
|
|
118
|
+
new_molecules[c[0]] = '.'.join(record['reagents'][x - lr] for x in c)
|
|
119
|
+
reagents.difference_update(c)
|
|
120
|
+
else:
|
|
121
|
+
log.append(f'impossible to contract different parts of reaction: {contract}')
|
|
122
|
+
for x in reactants:
|
|
123
|
+
new_molecules[x] = record['reactants'][x]
|
|
124
|
+
for x in products:
|
|
125
|
+
new_molecules[x] = record['products'][x - mol_count]
|
|
126
|
+
for x in reagents:
|
|
127
|
+
new_molecules[x] = record['reagents'][x - lr]
|
|
128
|
+
|
|
129
|
+
record['reactants'] = [x for x in new_molecules[:lr] if x is not None]
|
|
130
|
+
record['products'] = [x for x in new_molecules[-lp:] if x is not None]
|
|
131
|
+
record['reagents'] = [x for x in new_molecules[lr: -lp] if x is not None]
|
|
132
|
+
|
|
133
|
+
for k in ('reactants', 'products', 'reagents'):
|
|
134
|
+
tmp = []
|
|
135
|
+
for x in record[k]:
|
|
136
|
+
tmp.append(parser(smiles_tokenize(x), not ignore))
|
|
137
|
+
record[k] = tmp
|
|
138
|
+
|
|
139
|
+
if radicals:
|
|
140
|
+
atom_map = dict(enumerate(a for m in chain(record['reactants'], record['reagents'], record['products'])
|
|
141
|
+
for a in m['atoms']))
|
|
142
|
+
for x in radicals:
|
|
143
|
+
atom_map[x]['is_radical'] = True
|
|
144
|
+
|
|
145
|
+
postprocess_parsed_reaction(record, remap=remap, ignore=ignore)
|
|
146
|
+
rxn = create_reaction(record, ignore_bad_isotopes=ignore_bad_isotopes, keep_radicals=False,
|
|
147
|
+
ignore_carbon_radicals=ignore_carbon_radicals, keep_implicit=keep_implicit,
|
|
148
|
+
ignore_aromatic_radicals=ignore_aromatic_radicals, ignore=ignore,
|
|
149
|
+
_r_cls=_r_cls, _m_cls=_m_cls)
|
|
150
|
+
for mol, tmp in zip(rxn.molecules(), chain(record['reactants'], record['reagents'], record['products'])):
|
|
151
|
+
postprocess_molecule(mol, tmp, ignore_stereo=ignore_stereo)
|
|
152
|
+
return rxn
|
|
153
|
+
else:
|
|
154
|
+
record = parser(smiles_tokenize(smi), not ignore)
|
|
155
|
+
for x in radicals:
|
|
156
|
+
record['atoms'][x]['is_radical'] = True
|
|
157
|
+
record['log'].extend(log)
|
|
158
|
+
|
|
159
|
+
postprocess_parsed_molecule(record, remap=remap, ignore=ignore)
|
|
160
|
+
mol = create_molecule(record, ignore_bad_isotopes=ignore_bad_isotopes, keep_radicals=False,
|
|
161
|
+
ignore_carbon_radicals=ignore_carbon_radicals, keep_implicit=keep_implicit,
|
|
162
|
+
ignore_aromatic_radicals=ignore_aromatic_radicals, ignore=ignore,
|
|
163
|
+
_cls=_m_cls)
|
|
164
|
+
postprocess_molecule(mol, record, ignore_stereo=ignore_stereo)
|
|
165
|
+
return mol
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def postprocess_molecule(molecule, data, *, ignore_stereo=False):
|
|
169
|
+
mapping = data['mapping']
|
|
170
|
+
|
|
171
|
+
if ignore_stereo:
|
|
172
|
+
return
|
|
173
|
+
elif not data['stereo_atoms'] and not data['stereo_bonds']:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
atoms = molecule._atoms
|
|
177
|
+
st = molecule.stereogenic_tetrahedrons
|
|
178
|
+
sa = molecule.stereogenic_allenes
|
|
179
|
+
sat = molecule._stereo_allenes_terminals
|
|
180
|
+
ctc = molecule._stereo_cis_trans_counterpart
|
|
181
|
+
|
|
182
|
+
order = {mapping[n]: [mapping[m] for m in ms] for n, ms in data['order'].items()}
|
|
183
|
+
|
|
184
|
+
log = []
|
|
185
|
+
stereo = []
|
|
186
|
+
for i, s in data['stereo_atoms'].items():
|
|
187
|
+
n = mapping[i]
|
|
188
|
+
if not i and atoms[n].implicit_hydrogens: # first atom in smiles has reversed chiral mark
|
|
189
|
+
s = not s
|
|
190
|
+
|
|
191
|
+
if n in st:
|
|
192
|
+
stereo.append((molecule.add_atom_stereo, n, order[n], s))
|
|
193
|
+
elif n in sa:
|
|
194
|
+
t1, t2 = sat[n]
|
|
195
|
+
env = sa[n]
|
|
196
|
+
n1 = next(x for x in order[t1] if x in env)
|
|
197
|
+
n2 = next(x for x in order[t2] if x in env)
|
|
198
|
+
stereo.append((molecule.add_atom_stereo, n, (n1, n2), s))
|
|
199
|
+
else:
|
|
200
|
+
log.append(f'non chiral atom {n} has stereo label in smiles')
|
|
201
|
+
|
|
202
|
+
stereo_bonds = {mapping[n]: {mapping[m]: s for m, s in ms.items()}
|
|
203
|
+
for n, ms in data['stereo_bonds'].items()}
|
|
204
|
+
seen = set()
|
|
205
|
+
for n, ns in stereo_bonds.items():
|
|
206
|
+
if n in seen:
|
|
207
|
+
continue
|
|
208
|
+
if n in ctc:
|
|
209
|
+
m = ctc[n]
|
|
210
|
+
if m in stereo_bonds:
|
|
211
|
+
seen.add(m)
|
|
212
|
+
n2, s2 = stereo_bonds[m].popitem()
|
|
213
|
+
n1, s1 = ns.popitem()
|
|
214
|
+
stereo.append((molecule.add_cis_trans_stereo, n, m, n1, n2, s1 == s2))
|
|
215
|
+
|
|
216
|
+
while stereo:
|
|
217
|
+
fail_stereo = []
|
|
218
|
+
old_stereo = len(stereo)
|
|
219
|
+
for f, *args in stereo:
|
|
220
|
+
try:
|
|
221
|
+
f(*args, clean_cache=False)
|
|
222
|
+
except NotChiral:
|
|
223
|
+
fail_stereo.append((f, *args))
|
|
224
|
+
except IsChiral:
|
|
225
|
+
pass
|
|
226
|
+
except ValenceError:
|
|
227
|
+
log.append('structure has errors, stereo data skipped')
|
|
228
|
+
molecule.flush_cache()
|
|
229
|
+
break
|
|
230
|
+
else:
|
|
231
|
+
stereo = fail_stereo
|
|
232
|
+
if len(stereo) == old_stereo:
|
|
233
|
+
break
|
|
234
|
+
molecule.flush_stereo_cache()
|
|
235
|
+
continue
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
if log:
|
|
239
|
+
if 'chython_parsing_log' not in molecule.meta:
|
|
240
|
+
molecule.meta['chython_parsing_log'] = log
|
|
241
|
+
else:
|
|
242
|
+
molecule.meta['chython_parsing_log'].extend(log)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
__all__ = ['smiles']
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2025 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# This file is part of chython.
|
|
5
|
+
#
|
|
6
|
+
# chython is free software; you can redistribute it and/or modify
|
|
7
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
8
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
9
|
+
# (at your option) any later version.
|
|
10
|
+
#
|
|
11
|
+
# This program is distributed in the hope that it will be useful,
|
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
# GNU Lesser General Public License for more details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
17
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
#
|