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,251 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2020-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 re import compile
|
|
20
|
+
from ...exceptions import EmptyMolecule
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_stereo_collection = compile(r'MDLV30/STE(ABS|RAC|REL)(\d*)\s+ATOMS=\((.+)\)')
|
|
24
|
+
_v3000_token_re = compile(r'(?:[^\s"(]|"[^"]*"|\([^)]*\))+')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_mol_v3000(data, *, _header=True):
|
|
28
|
+
if _header:
|
|
29
|
+
title = data[0].strip() or None
|
|
30
|
+
data = data[4:]
|
|
31
|
+
else:
|
|
32
|
+
title = None
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
counts_tokens = data[1][13:].split()
|
|
36
|
+
atom_count = int(counts_tokens[0])
|
|
37
|
+
except (IndexError, ValueError):
|
|
38
|
+
raise ValueError(f'V3000: cannot parse counts line: {data[1]!r:.80}')
|
|
39
|
+
if not atom_count:
|
|
40
|
+
raise EmptyMolecule
|
|
41
|
+
try:
|
|
42
|
+
bonds_count = int(counts_tokens[1])
|
|
43
|
+
except (IndexError, ValueError):
|
|
44
|
+
raise ValueError(f'V3000: cannot parse bond count from counts line: {data[1]!r:.80}')
|
|
45
|
+
|
|
46
|
+
log = []
|
|
47
|
+
atoms = []
|
|
48
|
+
bonds = []
|
|
49
|
+
stereo = []
|
|
50
|
+
meta = {}
|
|
51
|
+
atom_map = {}
|
|
52
|
+
star_points = set()
|
|
53
|
+
|
|
54
|
+
for kv in counts_tokens[2:]:
|
|
55
|
+
if '=' in kv:
|
|
56
|
+
k, v = kv.split('=', 1)
|
|
57
|
+
if k and v:
|
|
58
|
+
meta[k] = v
|
|
59
|
+
|
|
60
|
+
# concatenate line continuations using list+join
|
|
61
|
+
tmp = []
|
|
62
|
+
parts = []
|
|
63
|
+
for line in data[3:]:
|
|
64
|
+
if line.endswith('-\n'):
|
|
65
|
+
parts.append(line[7:-2]) # skip `M V30 ` prefix and `-\n` suffix
|
|
66
|
+
else:
|
|
67
|
+
content = line[7:].rstrip()
|
|
68
|
+
if parts:
|
|
69
|
+
parts.append(content)
|
|
70
|
+
tmp.append(''.join(parts))
|
|
71
|
+
parts = []
|
|
72
|
+
else:
|
|
73
|
+
tmp.append(content.lstrip())
|
|
74
|
+
data = tmp
|
|
75
|
+
if len(data) < atom_count:
|
|
76
|
+
raise ValueError(f'V3000: expected {atom_count} atom lines but only {len(data)} lines available')
|
|
77
|
+
|
|
78
|
+
# parse atom block
|
|
79
|
+
_split = _v3000_token_re.findall
|
|
80
|
+
for i, line in enumerate(data[:atom_count], 1):
|
|
81
|
+
try:
|
|
82
|
+
tokens = _split(line)
|
|
83
|
+
n, a, x, y, z, m = tokens[0], tokens[1], tokens[2], tokens[3], tokens[4], tokens[5]
|
|
84
|
+
kvs = tokens[6:]
|
|
85
|
+
except (IndexError, ValueError):
|
|
86
|
+
raise ValueError(f'V3000 atom line {i}: cannot parse: {line!r:.80}')
|
|
87
|
+
|
|
88
|
+
if a.startswith(('[', 'NOT')):
|
|
89
|
+
raise ValueError(f'V3000 atom line {i}: list of atoms not supported')
|
|
90
|
+
elif a == '*':
|
|
91
|
+
star_points.add(n)
|
|
92
|
+
continue
|
|
93
|
+
elif a == 'R#':
|
|
94
|
+
raise ValueError(f'V3000 atom line {i}: R-groups not supported')
|
|
95
|
+
|
|
96
|
+
isotope = None
|
|
97
|
+
charge = 0
|
|
98
|
+
is_radical = False
|
|
99
|
+
for kv in kvs:
|
|
100
|
+
k, v = kv.split('=', 1)
|
|
101
|
+
if k == 'CHG':
|
|
102
|
+
charge = int(v)
|
|
103
|
+
elif k == 'MASS':
|
|
104
|
+
isotope = int(v)
|
|
105
|
+
elif k == 'RAD':
|
|
106
|
+
is_radical = True
|
|
107
|
+
if a == 'D':
|
|
108
|
+
if isotope:
|
|
109
|
+
raise ValueError(f'V3000 atom line {i}: isotope on deuterium atom')
|
|
110
|
+
a = 'H'
|
|
111
|
+
isotope = 2
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
fx, fy, fz = float(x), float(y), float(z)
|
|
115
|
+
except ValueError:
|
|
116
|
+
raise ValueError(f'V3000 atom line {i}: cannot parse coordinates from {x!r}, {y!r}, {z!r}')
|
|
117
|
+
|
|
118
|
+
atom_map[n] = len(atoms)
|
|
119
|
+
atoms.append({'element': a, 'isotope': isotope, 'charge': charge, 'is_radical': is_radical,
|
|
120
|
+
'x': fx, 'y': fy, 'z': fz, 'parsed_mapping': int(m)})
|
|
121
|
+
|
|
122
|
+
# parse bond block
|
|
123
|
+
bond_start = 2 + atom_count
|
|
124
|
+
bond_end = bond_start + bonds_count
|
|
125
|
+
if bond_end > len(data):
|
|
126
|
+
raise ValueError(f'V3000: expected {bonds_count} bond lines but only {len(data) - bond_start} available')
|
|
127
|
+
for i, line in enumerate(data[bond_start:bond_end], 1):
|
|
128
|
+
try:
|
|
129
|
+
tokens = _split(line)
|
|
130
|
+
_, t, a1, a2 = tokens[0], tokens[1], tokens[2], tokens[3]
|
|
131
|
+
kvs = tokens[4:]
|
|
132
|
+
except (IndexError, ValueError):
|
|
133
|
+
raise ValueError(f'V3000 bond line {i}: cannot parse: {line!r:.80}')
|
|
134
|
+
|
|
135
|
+
if a1 in star_points:
|
|
136
|
+
if a2 in star_points:
|
|
137
|
+
log.append('invalid bond ignored: star-point to star-point')
|
|
138
|
+
continue
|
|
139
|
+
try:
|
|
140
|
+
star = atom_map[a2]
|
|
141
|
+
except KeyError:
|
|
142
|
+
raise ValueError(f'V3000 bond line {i}: invalid atom number {a2}')
|
|
143
|
+
endpoints = None
|
|
144
|
+
elif a2 in star_points:
|
|
145
|
+
try:
|
|
146
|
+
star = atom_map[a1]
|
|
147
|
+
except KeyError:
|
|
148
|
+
raise ValueError(f'V3000 bond line {i}: invalid atom number {a1}')
|
|
149
|
+
endpoints = None
|
|
150
|
+
else:
|
|
151
|
+
star = None
|
|
152
|
+
try:
|
|
153
|
+
bt = int(t)
|
|
154
|
+
if bt in (9, 10):
|
|
155
|
+
bt = 8
|
|
156
|
+
log.append('coordinate bond replaced to special')
|
|
157
|
+
bonds.append((atom_map[a1], atom_map[a2], bt))
|
|
158
|
+
except KeyError:
|
|
159
|
+
raise ValueError(f'V3000 bond line {i}: invalid atom numbers {a1}, {a2}')
|
|
160
|
+
except ValueError:
|
|
161
|
+
raise ValueError(f'V3000 bond line {i}: invalid bond type {t!r}')
|
|
162
|
+
|
|
163
|
+
for kv in kvs:
|
|
164
|
+
k, v = kv.split('=')
|
|
165
|
+
if k == 'CFG':
|
|
166
|
+
if v == '1':
|
|
167
|
+
stereo.append((atom_map[a1], atom_map[a2], 1))
|
|
168
|
+
elif v == '3':
|
|
169
|
+
stereo.append((atom_map[a1], atom_map[a2], -1))
|
|
170
|
+
else:
|
|
171
|
+
log.append('invalid or unsupported stereo')
|
|
172
|
+
elif k == 'ENDPTS':
|
|
173
|
+
endpoints = v[1:-1].split()
|
|
174
|
+
if len(endpoints) != int(endpoints[0]) + 1:
|
|
175
|
+
raise ValueError(f'V3000 bond line {i}: invalid ENDPTS block')
|
|
176
|
+
if star is not None:
|
|
177
|
+
if endpoints: # noqa
|
|
178
|
+
for ep in endpoints[1:]: # noqa
|
|
179
|
+
try:
|
|
180
|
+
bonds.append((star, atom_map[ep], 8))
|
|
181
|
+
except KeyError:
|
|
182
|
+
raise ValueError(f'V3000 bond line {i}: invalid atom number {ep} in ENDPTS block')
|
|
183
|
+
else:
|
|
184
|
+
log.append('Bond ignored. Star atom not allowed as endpoint')
|
|
185
|
+
|
|
186
|
+
# parse remaining blocks (SGROUP, COLLECTION, etc.)
|
|
187
|
+
in_sgroup = False
|
|
188
|
+
in_collection = False
|
|
189
|
+
remaining_start = 3 + atom_count + bonds_count
|
|
190
|
+
for line in data[remaining_start:]:
|
|
191
|
+
if line.startswith('END CTAB'):
|
|
192
|
+
break
|
|
193
|
+
elif line.startswith('BEGIN SGROUP'):
|
|
194
|
+
in_sgroup = True
|
|
195
|
+
elif line.startswith('END SGROUP'):
|
|
196
|
+
in_sgroup = False
|
|
197
|
+
elif line.startswith('BEGIN COLLECTION'):
|
|
198
|
+
in_collection = True
|
|
199
|
+
elif line.startswith('END COLLECTION'):
|
|
200
|
+
in_collection = False
|
|
201
|
+
elif in_sgroup:
|
|
202
|
+
tokens = _split(line)
|
|
203
|
+
try:
|
|
204
|
+
_, _type, idx = tokens[0], tokens[1], tokens[2]
|
|
205
|
+
kvs = tokens[3:]
|
|
206
|
+
except IndexError:
|
|
207
|
+
log.append(f'V3000 SGROUP: cannot parse line: {line!r:.80}')
|
|
208
|
+
continue
|
|
209
|
+
if _type.startswith('DAT'):
|
|
210
|
+
a = f = d = None
|
|
211
|
+
for kv in kvs:
|
|
212
|
+
k, v = kv.split('=', 1)
|
|
213
|
+
if k == 'ATOMS':
|
|
214
|
+
a = tuple(atom_map[x] for x in v[1:-1].split()[1:] if x not in star_points)
|
|
215
|
+
elif k == 'FIELDNAME':
|
|
216
|
+
f = v.strip('"')
|
|
217
|
+
if k == 'FIELDDATA':
|
|
218
|
+
d = v.strip('"')
|
|
219
|
+
if a and f and d:
|
|
220
|
+
if f == 'MRV_IMPLICIT_H':
|
|
221
|
+
atoms[a[0]]['implicit_hydrogens'] = int(d[6:])
|
|
222
|
+
else:
|
|
223
|
+
log.append(f'ignored SGROUP DAT {idx}: {a}\t{f}\t{d}')
|
|
224
|
+
elif _type.startswith('SRU'):
|
|
225
|
+
raise ValueError('Polymers not supported')
|
|
226
|
+
elif in_collection:
|
|
227
|
+
m = _stereo_collection.search(line)
|
|
228
|
+
if m:
|
|
229
|
+
stype, gid, atom_list = m.group(1), m.group(2), m.group(3)
|
|
230
|
+
atom_indices = atom_list.split()
|
|
231
|
+
atom_indices = atom_indices[1:] # skip count
|
|
232
|
+
if stype == 'RAC': # AND group = positive
|
|
233
|
+
sg = int(gid) if gid else 1
|
|
234
|
+
for a in atom_indices:
|
|
235
|
+
try:
|
|
236
|
+
atoms[atom_map[a]]['extended_stereo'] = sg
|
|
237
|
+
except KeyError:
|
|
238
|
+
log.append(f'invalid atom in STERAC collection: {a}')
|
|
239
|
+
elif stype == 'REL': # OR group = negative
|
|
240
|
+
sg = -(int(gid) if gid else 1)
|
|
241
|
+
for a in atom_indices:
|
|
242
|
+
try:
|
|
243
|
+
atoms[atom_map[a]]['extended_stereo'] = sg
|
|
244
|
+
except KeyError:
|
|
245
|
+
log.append(f'invalid atom in STEREL collection: {a}')
|
|
246
|
+
# STEABS is the default (no extended_stereo needed)
|
|
247
|
+
|
|
248
|
+
return {'title': title, 'atoms': atoms, 'bonds': bonds, 'stereo': stereo, 'meta': meta, 'log': log}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
__all__ = ['parse_mol_v3000']
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2020-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 .emol import parse_mol_v3000
|
|
20
|
+
from ...exceptions import EmptyMolecule, EmptyReaction, InvalidV2000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_rxn_v3000(data, *, ignore=True):
|
|
24
|
+
tmp = data[4][13:].split()
|
|
25
|
+
reactants_count = int(tmp[0])
|
|
26
|
+
products_count = int(tmp[1]) + reactants_count
|
|
27
|
+
reagents_count = (int(tmp[2]) if len(tmp) == 3 else 0) + products_count
|
|
28
|
+
|
|
29
|
+
if not reagents_count:
|
|
30
|
+
raise EmptyReaction
|
|
31
|
+
|
|
32
|
+
title = data[1].strip() or None
|
|
33
|
+
log = []
|
|
34
|
+
molecules = []
|
|
35
|
+
|
|
36
|
+
start = 1
|
|
37
|
+
for n in range(0, reagents_count):
|
|
38
|
+
try:
|
|
39
|
+
start = next(n for n, x in enumerate(data[start + 5:], start + 5) if x.startswith('M V30 BEGIN CTAB'))
|
|
40
|
+
except StopIteration:
|
|
41
|
+
raise InvalidV2000
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
molecules.append(parse_mol_v3000(data[start:], _header=False))
|
|
45
|
+
except ValueError as e:
|
|
46
|
+
if isinstance(e, EmptyMolecule):
|
|
47
|
+
log.append(f'ignored empty molecule {n}')
|
|
48
|
+
elif ignore:
|
|
49
|
+
log.append(f'ignored molecule {n} with {e}')
|
|
50
|
+
else:
|
|
51
|
+
raise
|
|
52
|
+
|
|
53
|
+
if (lm := len(molecules)) < reactants_count:
|
|
54
|
+
reactants_count -= 1
|
|
55
|
+
products_count -= 1
|
|
56
|
+
reagents_count -= 1
|
|
57
|
+
elif lm < products_count:
|
|
58
|
+
products_count -= 1
|
|
59
|
+
reagents_count -= 1
|
|
60
|
+
else:
|
|
61
|
+
reagents_count -= 1
|
|
62
|
+
|
|
63
|
+
return {'reactants': molecules[:reactants_count], 'products': molecules[reactants_count:products_count],
|
|
64
|
+
'reagents': molecules[products_count:], 'title': title, 'log': log}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
__all__ = ['parse_rxn_v3000']
|
chython/files/mdl/mol.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2020-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 ...exceptions import EmptyMolecule, InvalidCharge, InvalidV2000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_ctf_data = {'R': 'is_radical', 'C': 'charge', 'I': 'isotope'}
|
|
23
|
+
_charge_map = {' 0': 0, ' 1': 3, ' 2': 2, ' 3': 1, ' 4': 0, ' 5': -1, ' 6': -2, ' 7': -3}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_mol_v2000(data):
|
|
27
|
+
line = data[3]
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
atoms_count = int(line[0:3])
|
|
31
|
+
except (ValueError, IndexError):
|
|
32
|
+
raise InvalidV2000(f'V2000 counts line: cannot parse atom count from {line!r:.80}')
|
|
33
|
+
try:
|
|
34
|
+
bonds_count = int(line[3:6])
|
|
35
|
+
except (ValueError, IndexError):
|
|
36
|
+
raise InvalidV2000(f'V2000 counts line: cannot parse bond count from {line!r:.80}')
|
|
37
|
+
|
|
38
|
+
if not atoms_count:
|
|
39
|
+
raise EmptyMolecule
|
|
40
|
+
|
|
41
|
+
available_lines = len(data) - 4
|
|
42
|
+
if available_lines < atoms_count:
|
|
43
|
+
raise InvalidV2000(f'V2000: expected {atoms_count} atom lines but only {available_lines} lines available')
|
|
44
|
+
if available_lines < atoms_count + bonds_count:
|
|
45
|
+
raise InvalidV2000(f'V2000: expected {bonds_count} bond lines but only {available_lines - atoms_count} '
|
|
46
|
+
f'lines available after atom block')
|
|
47
|
+
|
|
48
|
+
log = []
|
|
49
|
+
title = data[0].strip() or None
|
|
50
|
+
atoms = []
|
|
51
|
+
bonds = []
|
|
52
|
+
stereo = []
|
|
53
|
+
dat = {}
|
|
54
|
+
|
|
55
|
+
# parse atom block
|
|
56
|
+
_cm = _charge_map
|
|
57
|
+
for i, line in enumerate(data[4: 4 + atoms_count], 1):
|
|
58
|
+
try:
|
|
59
|
+
charge = _cm[line[36:39]]
|
|
60
|
+
except KeyError:
|
|
61
|
+
raise InvalidCharge(f'V2000 atom line {i}: invalid charge field {line[36:39]!r} in: {line!r:.80}')
|
|
62
|
+
|
|
63
|
+
element = line[31:34].strip()
|
|
64
|
+
isotope = line[34:36]
|
|
65
|
+
|
|
66
|
+
if element in 'AL':
|
|
67
|
+
raise ValueError(f'V2000 atom line {i}: queries not supported')
|
|
68
|
+
elif element == 'D':
|
|
69
|
+
element = 'H'
|
|
70
|
+
if isotope != ' 0':
|
|
71
|
+
raise ValueError(f'V2000 atom line {i}: isotope on deuterium atom')
|
|
72
|
+
isotope = 2
|
|
73
|
+
delta_isotope = None
|
|
74
|
+
elif isotope != ' 0':
|
|
75
|
+
delta_isotope = int(isotope)
|
|
76
|
+
isotope = None
|
|
77
|
+
else:
|
|
78
|
+
isotope = None
|
|
79
|
+
delta_isotope = None
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
x = float(line[0:10])
|
|
83
|
+
y = float(line[10:20])
|
|
84
|
+
z = float(line[20:30])
|
|
85
|
+
except ValueError:
|
|
86
|
+
raise InvalidV2000(f'V2000 atom line {i}: cannot parse coordinates from {line[0:30]!r}')
|
|
87
|
+
|
|
88
|
+
mapping = line[60:63]
|
|
89
|
+
atoms.append({'element': element, 'charge': charge, 'isotope': isotope,
|
|
90
|
+
'parsed_mapping': int(mapping) if mapping.strip() else 0,
|
|
91
|
+
'x': x, 'y': y, 'z': z, 'delta_isotope': delta_isotope})
|
|
92
|
+
|
|
93
|
+
# parse bond block
|
|
94
|
+
for i, line in enumerate(data[4 + atoms_count: 4 + atoms_count + bonds_count], 1):
|
|
95
|
+
try:
|
|
96
|
+
a1 = int(line[0:3]) - 1
|
|
97
|
+
a2 = int(line[3:6]) - 1
|
|
98
|
+
except ValueError:
|
|
99
|
+
raise InvalidV2000(f'V2000 bond line {i}: cannot parse atom indices from {line[0:6]!r}')
|
|
100
|
+
if a1 < 0 or a1 >= atoms_count or a2 < 0 or a2 >= atoms_count:
|
|
101
|
+
raise InvalidV2000(f'V2000 bond line {i}: atom index out of range '
|
|
102
|
+
f'(a1={a1 + 1}, a2={a2 + 1}, atoms={atoms_count})')
|
|
103
|
+
s = line[9:12]
|
|
104
|
+
if s == ' 1':
|
|
105
|
+
stereo.append((a1, a2, 1))
|
|
106
|
+
elif s == ' 6':
|
|
107
|
+
stereo.append((a1, a2, -1))
|
|
108
|
+
elif s != ' 0':
|
|
109
|
+
log.append(f'unsupported or invalid stereo: {line}')
|
|
110
|
+
b = int(line[6:9])
|
|
111
|
+
if b == 9:
|
|
112
|
+
b = 8
|
|
113
|
+
log.append(f'coordinate bond replaced with special: {line}')
|
|
114
|
+
bonds.append((a1, a2, b))
|
|
115
|
+
|
|
116
|
+
# parse properties block
|
|
117
|
+
for line in data[4 + atoms_count + bonds_count:]:
|
|
118
|
+
if line.startswith('M END'):
|
|
119
|
+
break
|
|
120
|
+
elif line.startswith('M ALS'):
|
|
121
|
+
raise ValueError('list of atoms not supported')
|
|
122
|
+
elif line.startswith(('M ISO', 'M RAD', 'M CHG')):
|
|
123
|
+
_type = _ctf_data[line[3]]
|
|
124
|
+
try:
|
|
125
|
+
count = int(line[6:9])
|
|
126
|
+
except ValueError:
|
|
127
|
+
raise InvalidV2000(f'V2000 properties: cannot parse entry count from {line!r:.80}')
|
|
128
|
+
for i in range(count):
|
|
129
|
+
i8 = i * 8
|
|
130
|
+
try:
|
|
131
|
+
atom = int(line[10 + i8:13 + i8])
|
|
132
|
+
except ValueError:
|
|
133
|
+
raise InvalidV2000(f'V2000 properties: cannot parse atom number in {line!r:.80}')
|
|
134
|
+
if not atom or atom > atoms_count:
|
|
135
|
+
raise InvalidV2000(f'V2000 properties: atom number {atom} out of range [1, {atoms_count}] '
|
|
136
|
+
f'in: {line!r:.80}')
|
|
137
|
+
atom = atoms[atom - 1]
|
|
138
|
+
atom[_type] = int(line[14 + i8:17 + i8])
|
|
139
|
+
|
|
140
|
+
elif line.startswith('M STY'):
|
|
141
|
+
for i in range(int(line[6:9])):
|
|
142
|
+
i8 = i * 8
|
|
143
|
+
if (st := line[14 + i8:17 + i8]) == 'DAT':
|
|
144
|
+
dat[int(line[10 + i8:13 + i8])] = {}
|
|
145
|
+
elif st == 'SUP':
|
|
146
|
+
dat[int(line[10 + i8:13 + i8])] = {'type': 'MDL_SUP'}
|
|
147
|
+
elif line.startswith('M SAL'):
|
|
148
|
+
i = int(line[7:10])
|
|
149
|
+
if i in dat:
|
|
150
|
+
dat[i]['atoms'] = tuple(int(line[14 + 4 * i:17 + 4 * i]) - 1 for i in range(int(line[10:13])))
|
|
151
|
+
elif line.startswith('M SDT'):
|
|
152
|
+
i = int(line[7:10])
|
|
153
|
+
if i in dat:
|
|
154
|
+
dat[i]['type'] = line.split()[-1].lower()
|
|
155
|
+
elif line.startswith('M SED'):
|
|
156
|
+
i = int(line[7:10])
|
|
157
|
+
if i in dat:
|
|
158
|
+
dat[i]['value'] = line[10:].strip().replace('/', '').lower()
|
|
159
|
+
elif line.startswith('M SMT'):
|
|
160
|
+
i = int(line[7:10])
|
|
161
|
+
if i in dat:
|
|
162
|
+
dat[i]['value'] = line[10:].strip()
|
|
163
|
+
elif not line.startswith('M SDD'):
|
|
164
|
+
log.append(f'ignored line: {line}')
|
|
165
|
+
|
|
166
|
+
for a in atoms:
|
|
167
|
+
if 'is_radical' in a:
|
|
168
|
+
a['is_radical'] = True
|
|
169
|
+
for x in dat.values():
|
|
170
|
+
try:
|
|
171
|
+
_type = x['type']
|
|
172
|
+
if _type == 'mrv_implicit_h':
|
|
173
|
+
_atoms = x['atoms']
|
|
174
|
+
value = x['value']
|
|
175
|
+
if len(_atoms) != 1 or _atoms[0] == -1 or not value:
|
|
176
|
+
raise InvalidV2000(f'MRV_IMPLICIT_H spec invalid {x}')
|
|
177
|
+
atoms[_atoms[0]]['implicit_hydrogens'] = int(value[6:])
|
|
178
|
+
else:
|
|
179
|
+
log.append(f'ignored data: {x}')
|
|
180
|
+
except KeyError:
|
|
181
|
+
raise InvalidV2000(f'Invalid SGROUP {x}')
|
|
182
|
+
|
|
183
|
+
return {'title': title, 'atoms': atoms, 'bonds': bonds, 'stereo': stereo, 'log': log}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
__all__ = ['parse_mol_v2000']
|