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,209 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2020-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 ...exceptions import EmptyMolecule
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_mol_v3000(data, *, _header=True):
|
|
23
|
+
if _header:
|
|
24
|
+
title = data[0].strip() or None
|
|
25
|
+
data = data[4:]
|
|
26
|
+
else:
|
|
27
|
+
title = None
|
|
28
|
+
|
|
29
|
+
atom_count, bonds_count, *kvs = data[1][13:].split()
|
|
30
|
+
atom_count = int(atom_count)
|
|
31
|
+
if not atom_count:
|
|
32
|
+
raise EmptyMolecule
|
|
33
|
+
bonds_count = int(bonds_count)
|
|
34
|
+
|
|
35
|
+
log = []
|
|
36
|
+
atoms = []
|
|
37
|
+
bonds = []
|
|
38
|
+
stereo = []
|
|
39
|
+
meta = {}
|
|
40
|
+
atom_map = {}
|
|
41
|
+
star_points = []
|
|
42
|
+
|
|
43
|
+
for kv in kvs:
|
|
44
|
+
if '=' in kv:
|
|
45
|
+
k, v = kv.split('=', 1)
|
|
46
|
+
if k and v:
|
|
47
|
+
meta[k] = v
|
|
48
|
+
|
|
49
|
+
# concatenate line breaks
|
|
50
|
+
tmp = []
|
|
51
|
+
keep = None
|
|
52
|
+
for line in data[3:]:
|
|
53
|
+
if line.endswith('-\n'):
|
|
54
|
+
line = line[7:-2] # skip `M V30 ` and `-\n`
|
|
55
|
+
if keep:
|
|
56
|
+
keep += line
|
|
57
|
+
else:
|
|
58
|
+
keep = line.lstrip()
|
|
59
|
+
else:
|
|
60
|
+
line = line[7:] # skip `M V30 `
|
|
61
|
+
if keep:
|
|
62
|
+
tmp.append(keep + line.rstrip())
|
|
63
|
+
keep = None
|
|
64
|
+
else:
|
|
65
|
+
tmp.append(line.strip())
|
|
66
|
+
data = tmp
|
|
67
|
+
|
|
68
|
+
for line in data[:atom_count]:
|
|
69
|
+
n, a, x, y, z, m, *kvs = split(line)
|
|
70
|
+
if a.startswith(('[', 'NOT')):
|
|
71
|
+
raise ValueError('list of atoms not supported')
|
|
72
|
+
elif a == '*':
|
|
73
|
+
star_points.append(n)
|
|
74
|
+
continue
|
|
75
|
+
elif a == 'R#':
|
|
76
|
+
raise ValueError('R-groups not supported')
|
|
77
|
+
|
|
78
|
+
i = None
|
|
79
|
+
c = 0
|
|
80
|
+
r = False
|
|
81
|
+
for kv in kvs:
|
|
82
|
+
k, v = kv.split('=', 1)
|
|
83
|
+
if k == 'CHG':
|
|
84
|
+
c = int(v)
|
|
85
|
+
elif k == 'MASS':
|
|
86
|
+
i = int(v)
|
|
87
|
+
elif k == 'RAD':
|
|
88
|
+
r = True
|
|
89
|
+
if a == 'D':
|
|
90
|
+
if i:
|
|
91
|
+
raise ValueError('isotope on deuterium atom')
|
|
92
|
+
a = 'H'
|
|
93
|
+
i = 2
|
|
94
|
+
|
|
95
|
+
atom_map[n] = len(atoms)
|
|
96
|
+
atoms.append({'element': a, 'isotope': i, 'charge': c, 'is_radical': r,
|
|
97
|
+
'x': float(x), 'y': float(y), 'z': float(z), 'parsed_mapping': int(m)})
|
|
98
|
+
|
|
99
|
+
for line in data[2 + atom_count: 2 + atom_count + bonds_count]:
|
|
100
|
+
_, t, a1, a2, *kvs = split(line)
|
|
101
|
+
if a1 in star_points:
|
|
102
|
+
if a2 in star_points:
|
|
103
|
+
log.append('invalid bond ignored: star-point to star-point')
|
|
104
|
+
continue
|
|
105
|
+
try:
|
|
106
|
+
star = atom_map[a2]
|
|
107
|
+
except KeyError:
|
|
108
|
+
raise ValueError('invalid atoms number')
|
|
109
|
+
endpoints = None
|
|
110
|
+
elif a2 in star_points:
|
|
111
|
+
try:
|
|
112
|
+
star = atom_map[a1]
|
|
113
|
+
except KeyError:
|
|
114
|
+
raise ValueError('invalid atoms number')
|
|
115
|
+
endpoints = None
|
|
116
|
+
else:
|
|
117
|
+
star = None
|
|
118
|
+
try:
|
|
119
|
+
t = int(t)
|
|
120
|
+
if t in (9, 10): # added ad-hoc for bond type 9
|
|
121
|
+
t = 8
|
|
122
|
+
log.append('coordinate bond replaced to special')
|
|
123
|
+
bonds.append((atom_map[a1], atom_map[a2], t))
|
|
124
|
+
except KeyError:
|
|
125
|
+
raise ValueError('invalid atoms numbers')
|
|
126
|
+
|
|
127
|
+
for kv in kvs:
|
|
128
|
+
k, v = kv.split('=')
|
|
129
|
+
if k == 'CFG':
|
|
130
|
+
if v == '1':
|
|
131
|
+
stereo.append((atom_map[a1], atom_map[a2], 1))
|
|
132
|
+
elif v == '3':
|
|
133
|
+
stereo.append((atom_map[a1], atom_map[a2], -1))
|
|
134
|
+
else:
|
|
135
|
+
log.append('invalid or unsupported stereo')
|
|
136
|
+
elif k == 'ENDPTS':
|
|
137
|
+
endpoints = v[1:-1].split()
|
|
138
|
+
if len(endpoints) != int(endpoints[0]) + 1:
|
|
139
|
+
raise ValueError('invalid ENDPTS block')
|
|
140
|
+
if star is not None:
|
|
141
|
+
if endpoints: # noqa
|
|
142
|
+
for m in endpoints[1:]: # noqa
|
|
143
|
+
try:
|
|
144
|
+
bonds.append((star, atom_map[m], 8))
|
|
145
|
+
except KeyError:
|
|
146
|
+
raise ValueError('invalid atoms numbers in ENDPTS block')
|
|
147
|
+
else:
|
|
148
|
+
log.append('Bond ignored. Star atom not allowed as endpoint')
|
|
149
|
+
|
|
150
|
+
drop = True
|
|
151
|
+
for line in data[3 + atom_count + bonds_count:]:
|
|
152
|
+
if line.startswith('END CTAB'):
|
|
153
|
+
break
|
|
154
|
+
elif drop:
|
|
155
|
+
if line.startswith('BEGIN SGROUP'):
|
|
156
|
+
drop = False
|
|
157
|
+
continue
|
|
158
|
+
elif line.startswith('END SGROUP'):
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
_, _type, i, *kvs = split(line)
|
|
162
|
+
if _type.startswith('DAT'):
|
|
163
|
+
a = f = d = None
|
|
164
|
+
for kv in kvs:
|
|
165
|
+
k, v = kv.split('=', 1)
|
|
166
|
+
if k == 'ATOMS':
|
|
167
|
+
a = tuple(atom_map[x] for x in v[1:-1].split()[1:] if x not in star_points)
|
|
168
|
+
elif k == 'FIELDNAME':
|
|
169
|
+
f = v.strip('"')
|
|
170
|
+
if k == 'FIELDDATA':
|
|
171
|
+
d = v.strip('"')
|
|
172
|
+
if a and f and d:
|
|
173
|
+
if f == 'MRV_IMPLICIT_H':
|
|
174
|
+
atoms[a[0]]['implicit_hydrogens'] = int(d[6:])
|
|
175
|
+
else:
|
|
176
|
+
log.append(f'ignored SGROUP DAT {i}: {a}\t{f}\t{d}')
|
|
177
|
+
elif _type.startswith('SRU'):
|
|
178
|
+
raise ValueError('Polymers not supported')
|
|
179
|
+
|
|
180
|
+
return {'title': title, 'atoms': atoms, 'bonds': bonds, 'stereo': stereo, 'meta': meta, 'log': log}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def split(line): # todo optimize
|
|
184
|
+
collect = []
|
|
185
|
+
tmp = []
|
|
186
|
+
until = None
|
|
187
|
+
for s in line:
|
|
188
|
+
if until:
|
|
189
|
+
tmp.append(s)
|
|
190
|
+
if s == until:
|
|
191
|
+
until = None
|
|
192
|
+
elif s == '(':
|
|
193
|
+
tmp.append('(')
|
|
194
|
+
until = ')'
|
|
195
|
+
elif s == '"':
|
|
196
|
+
tmp.append(s)
|
|
197
|
+
until = '"'
|
|
198
|
+
elif s == ' ':
|
|
199
|
+
if tmp:
|
|
200
|
+
collect.append(''.join(tmp))
|
|
201
|
+
tmp = []
|
|
202
|
+
else:
|
|
203
|
+
tmp.append(s)
|
|
204
|
+
if tmp:
|
|
205
|
+
collect.append(''.join(tmp))
|
|
206
|
+
return collect
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
__all__ = ['parse_mol_v3000']
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2020-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 .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,144 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2020-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 ...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
|
+
atoms_count = int(line[0:3])
|
|
30
|
+
bonds_count = int(line[3:6])
|
|
31
|
+
if not atoms_count:
|
|
32
|
+
raise EmptyMolecule
|
|
33
|
+
|
|
34
|
+
log = []
|
|
35
|
+
title = data[0].strip() or None
|
|
36
|
+
atoms = []
|
|
37
|
+
bonds = []
|
|
38
|
+
stereo = []
|
|
39
|
+
dat = {}
|
|
40
|
+
|
|
41
|
+
for line in data[4: 4 + atoms_count]:
|
|
42
|
+
try:
|
|
43
|
+
charge = _charge_map[line[36:39]]
|
|
44
|
+
except KeyError:
|
|
45
|
+
raise InvalidCharge
|
|
46
|
+
element = line[31:34].strip()
|
|
47
|
+
isotope = line[34:36]
|
|
48
|
+
delta_isotope = None
|
|
49
|
+
|
|
50
|
+
if element in 'AL':
|
|
51
|
+
raise ValueError('queries not supported')
|
|
52
|
+
elif element == 'D':
|
|
53
|
+
element = 'H'
|
|
54
|
+
if isotope != ' 0':
|
|
55
|
+
raise ValueError('isotope on deuterium atom')
|
|
56
|
+
isotope = 2
|
|
57
|
+
elif isotope != ' 0':
|
|
58
|
+
delta_isotope = int(isotope)
|
|
59
|
+
isotope = None
|
|
60
|
+
else:
|
|
61
|
+
isotope = None
|
|
62
|
+
|
|
63
|
+
mapping = line[60:63]
|
|
64
|
+
atoms.append({'element': element, 'charge': charge, 'isotope': isotope,
|
|
65
|
+
'parsed_mapping': int(mapping) if mapping else 0, 'x': float(line[0:10]), 'y': float(line[10:20]),
|
|
66
|
+
'z': float(line[20:30]), 'delta_isotope': delta_isotope})
|
|
67
|
+
|
|
68
|
+
for line in data[4 + atoms_count: 4 + atoms_count + bonds_count]:
|
|
69
|
+
a1, a2 = int(line[0:3]) - 1, int(line[3:6]) - 1
|
|
70
|
+
s = line[9:12]
|
|
71
|
+
if s == ' 1':
|
|
72
|
+
stereo.append((a1, a2, 1))
|
|
73
|
+
elif s == ' 6':
|
|
74
|
+
stereo.append((a1, a2, -1))
|
|
75
|
+
elif s != ' 0':
|
|
76
|
+
log.append(f'unsupported or invalid stereo: {line}')
|
|
77
|
+
b = int(line[6:9])
|
|
78
|
+
if b == 9: # added ad-hoc for bond type 9
|
|
79
|
+
b = 8
|
|
80
|
+
log.append(f'coordinate bond replaced with special: {line}')
|
|
81
|
+
bonds.append((a1, a2, b))
|
|
82
|
+
|
|
83
|
+
for line in data[4 + atoms_count + bonds_count:]:
|
|
84
|
+
if line.startswith('M END'):
|
|
85
|
+
break
|
|
86
|
+
elif line.startswith('M ALS'):
|
|
87
|
+
raise ValueError('list of atoms not supported')
|
|
88
|
+
elif line.startswith(('M ISO', 'M RAD', 'M CHG')):
|
|
89
|
+
_type = _ctf_data[line[3]]
|
|
90
|
+
for i in range(int(line[6:9])):
|
|
91
|
+
i8 = i * 8
|
|
92
|
+
atom = int(line[10 + i8:13 + i8])
|
|
93
|
+
if not atom or atom > len(atoms):
|
|
94
|
+
raise InvalidV2000('invalid atoms number')
|
|
95
|
+
atom = atoms[atom - 1]
|
|
96
|
+
atom[_type] = int(line[14 + i8:17 + i8])
|
|
97
|
+
|
|
98
|
+
elif line.startswith('M STY'):
|
|
99
|
+
for i in range(int(line[6:9])):
|
|
100
|
+
i8 = i * 8
|
|
101
|
+
if (st := line[14 + i8:17 + i8]) == 'DAT':
|
|
102
|
+
dat[int(line[10 + i8:13 + i8])] = {}
|
|
103
|
+
elif st == 'SUP':
|
|
104
|
+
dat[int(line[10 + i8:13 + i8])] = {'type': 'MDL_SUP'}
|
|
105
|
+
elif line.startswith('M SAL'):
|
|
106
|
+
i = int(line[7:10])
|
|
107
|
+
if i in dat:
|
|
108
|
+
dat[i]['atoms'] = tuple(int(line[14 + 4 * i:17 + 4 * i]) - 1 for i in range(int(line[10:13])))
|
|
109
|
+
elif line.startswith('M SDT'):
|
|
110
|
+
i = int(line[7:10])
|
|
111
|
+
if i in dat:
|
|
112
|
+
dat[i]['type'] = line.split()[-1].lower()
|
|
113
|
+
elif line.startswith('M SED'):
|
|
114
|
+
i = int(line[7:10])
|
|
115
|
+
if i in dat:
|
|
116
|
+
dat[i]['value'] = line[10:].strip().replace('/', '').lower()
|
|
117
|
+
elif line.startswith('M SMT'):
|
|
118
|
+
i = int(line[7:10])
|
|
119
|
+
if i in dat:
|
|
120
|
+
dat[i]['value'] = line[10:].strip()
|
|
121
|
+
elif not line.startswith('M SDD'):
|
|
122
|
+
log.append(f'ignored line: {line}')
|
|
123
|
+
|
|
124
|
+
for a in atoms:
|
|
125
|
+
if 'is_radical' in a: # int to bool
|
|
126
|
+
a['is_radical'] = True
|
|
127
|
+
for x in dat.values():
|
|
128
|
+
try:
|
|
129
|
+
_type = x['type']
|
|
130
|
+
if _type == 'mrv_implicit_h':
|
|
131
|
+
_atoms = x['atoms']
|
|
132
|
+
value = x['value']
|
|
133
|
+
if len(_atoms) != 1 or _atoms[0] == -1 or not value:
|
|
134
|
+
raise InvalidV2000(f'MRV_IMPLICIT_H spec invalid {x}')
|
|
135
|
+
atoms[_atoms[0]]['implicit_hydrogens'] = int(value[6:])
|
|
136
|
+
else:
|
|
137
|
+
log.append(f'ignored data: {x}')
|
|
138
|
+
except KeyError:
|
|
139
|
+
raise InvalidV2000(f'Invalid SGROUP {x}')
|
|
140
|
+
|
|
141
|
+
return {'title': title, 'atoms': atoms, 'bonds': bonds, 'stereo': stereo, 'log': log}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
__all__ = ['parse_mol_v2000']
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2021-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 abc import ABCMeta, abstractmethod
|
|
20
|
+
from base64 import urlsafe_b64encode
|
|
21
|
+
from fileinput import FileInput
|
|
22
|
+
from io import StringIO, TextIOWrapper
|
|
23
|
+
from itertools import islice
|
|
24
|
+
from os.path import abspath, join
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from pickle import load, UnpicklingError
|
|
27
|
+
from sys import platform
|
|
28
|
+
from tempfile import gettempdir
|
|
29
|
+
from typing import Union, Iterator, List, Dict, Optional
|
|
30
|
+
from ...containers import ReactionContainer, MoleculeContainer
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MDLReadMeta(ABCMeta):
|
|
34
|
+
def __call__(cls, *args, **kwargs):
|
|
35
|
+
if kwargs.get('indexable'):
|
|
36
|
+
_cls = type(cls.__name__, (cls,), {'__len__': lambda x: len(x._shifts), '__module__': cls.__module__})
|
|
37
|
+
obj = object.__new__(_cls) # noqa
|
|
38
|
+
else:
|
|
39
|
+
obj = object.__new__(cls) # noqa
|
|
40
|
+
obj.__init__(*args, **kwargs)
|
|
41
|
+
return obj
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MDLRead(metaclass=MDLReadMeta):
|
|
45
|
+
def __init__(self, file, buffer_size=1000, indexable=False, ignore=True, remap=False, ignore_bad_isotopes=False,
|
|
46
|
+
ignore_stereo=False, calc_cis_trans=False):
|
|
47
|
+
if isinstance(file, str):
|
|
48
|
+
self._file = open(file)
|
|
49
|
+
self._is_buffer = False
|
|
50
|
+
elif isinstance(file, Path):
|
|
51
|
+
self._file = file.open()
|
|
52
|
+
self._is_buffer = False
|
|
53
|
+
elif isinstance(file, (TextIOWrapper, StringIO, FileInput)):
|
|
54
|
+
self._file = file
|
|
55
|
+
self._is_buffer = True
|
|
56
|
+
else:
|
|
57
|
+
raise TypeError('invalid file. TextIOWrapper, StringIO or FileInput subclasses or path to file expected')
|
|
58
|
+
self._shifts = None
|
|
59
|
+
self._tell = 0
|
|
60
|
+
self._buffer_size = buffer_size
|
|
61
|
+
self._buffer = None
|
|
62
|
+
self._ignore = ignore
|
|
63
|
+
self._remap = remap
|
|
64
|
+
self._ignore_bad_isotopes = ignore_bad_isotopes
|
|
65
|
+
self._ignore_stereo = ignore_stereo
|
|
66
|
+
self._calc_cis_trans = calc_cis_trans
|
|
67
|
+
if indexable:
|
|
68
|
+
self._load_cache()
|
|
69
|
+
|
|
70
|
+
def read(self, amount: Optional[int] = None) -> List[Union[ReactionContainer, MoleculeContainer]]:
|
|
71
|
+
"""
|
|
72
|
+
Parse whole file
|
|
73
|
+
|
|
74
|
+
:param amount: number of records to read
|
|
75
|
+
"""
|
|
76
|
+
if amount:
|
|
77
|
+
return list(islice(iter(self), amount))
|
|
78
|
+
return list(iter(self))
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def read_structure(self, *, current: bool = True):
|
|
82
|
+
"""
|
|
83
|
+
Read Reaction or Molecule container.
|
|
84
|
+
|
|
85
|
+
:param current: return current structure if already parsed, otherwise read next
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def read_metadata(self, *, current: bool = True) -> Dict[str, str]:
|
|
90
|
+
"""
|
|
91
|
+
Read metadata block
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def close(self, force: bool = False):
|
|
95
|
+
"""
|
|
96
|
+
Close opened file
|
|
97
|
+
|
|
98
|
+
:param force: force closing of externally opened file or buffer
|
|
99
|
+
"""
|
|
100
|
+
if not self._is_buffer or force:
|
|
101
|
+
self._file.close()
|
|
102
|
+
|
|
103
|
+
def tell(self):
|
|
104
|
+
"""
|
|
105
|
+
Number of records processed from the original file
|
|
106
|
+
"""
|
|
107
|
+
return self._tell
|
|
108
|
+
|
|
109
|
+
def seek(self, offset):
|
|
110
|
+
"""
|
|
111
|
+
Shift to a given record number
|
|
112
|
+
"""
|
|
113
|
+
if self._shifts:
|
|
114
|
+
if 0 <= offset < len(self._shifts):
|
|
115
|
+
self._tell = offset
|
|
116
|
+
self._buffer = None
|
|
117
|
+
self._file.seek(self._shifts[offset])
|
|
118
|
+
else:
|
|
119
|
+
raise IndexError('invalid offset')
|
|
120
|
+
else:
|
|
121
|
+
raise NotImplementedError('Indexable supported in unix-like o.s. and for files stored on disk')
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def reset_index(self):
|
|
125
|
+
"""
|
|
126
|
+
Create (rewrite) indexation table. Implemented only for object that
|
|
127
|
+
is a real file (the path to the file is specified) because the external grep utility is used.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def read_block(self, *, current: bool = True) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Read full record block with metadata
|
|
133
|
+
"""
|
|
134
|
+
return ''.join(self._read_block(current=current))
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def _read_block(self, *, current: bool = True) -> List[str]:
|
|
138
|
+
"""
|
|
139
|
+
Read full record block with metadata
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def _load_cache(self):
|
|
143
|
+
"""
|
|
144
|
+
Load existing cache or create new. Working only for UNIX-like systems and local files (not buffers).
|
|
145
|
+
"""
|
|
146
|
+
if platform == 'win32' or self._is_buffer:
|
|
147
|
+
return
|
|
148
|
+
try:
|
|
149
|
+
with open(self._cache_path, 'rb') as f:
|
|
150
|
+
self._shifts = load(f)
|
|
151
|
+
except FileNotFoundError: # cache not found
|
|
152
|
+
self.reset_index()
|
|
153
|
+
except IsADirectoryError as e:
|
|
154
|
+
raise IsADirectoryError(f'Please delete {self._cache_path} directory') from e
|
|
155
|
+
except (UnpicklingError, EOFError) as e: # invalid file. ask user to check it.
|
|
156
|
+
raise UnpicklingError(f'Invalid cache file {self._cache_path}. Please delete it') from e
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def _cache_path(self):
|
|
160
|
+
return abspath(join(gettempdir(), 'chython_' + urlsafe_b64encode(abspath(self._file.name).encode()).decode()))
|
|
161
|
+
|
|
162
|
+
def __enter__(self):
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def __exit__(self, _type, value, traceback):
|
|
166
|
+
self.close()
|
|
167
|
+
|
|
168
|
+
def __iter__(self) -> Iterator[Union[ReactionContainer, MoleculeContainer]]:
|
|
169
|
+
while True:
|
|
170
|
+
try:
|
|
171
|
+
yield self.read_structure(current=False)
|
|
172
|
+
except ValueError:
|
|
173
|
+
pass
|
|
174
|
+
except EOFError:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
def __next__(self) -> Union[ReactionContainer, MoleculeContainer]:
|
|
178
|
+
return next(iter(self))
|
|
179
|
+
|
|
180
|
+
def __getitem__(self, item) -> Union[ReactionContainer, MoleculeContainer,
|
|
181
|
+
List[Union[ReactionContainer, MoleculeContainer]]]:
|
|
182
|
+
"""
|
|
183
|
+
Getting the item by index from the original file,
|
|
184
|
+
For slices records with errors skipped.
|
|
185
|
+
"""
|
|
186
|
+
if self._shifts:
|
|
187
|
+
_len = len(self._shifts)
|
|
188
|
+
if isinstance(item, int):
|
|
189
|
+
if item >= _len or item < -_len:
|
|
190
|
+
raise IndexError('List index out of range')
|
|
191
|
+
if item < 0:
|
|
192
|
+
item += _len
|
|
193
|
+
self.seek(item)
|
|
194
|
+
return self.read_structure()
|
|
195
|
+
elif isinstance(item, slice):
|
|
196
|
+
start, stop, step = item.indices(_len)
|
|
197
|
+
if start == stop:
|
|
198
|
+
return []
|
|
199
|
+
if step == 1:
|
|
200
|
+
self.seek(start)
|
|
201
|
+
records = []
|
|
202
|
+
for _ in range(start, stop):
|
|
203
|
+
try:
|
|
204
|
+
records.append(self.read_structure(current=False))
|
|
205
|
+
except EOFError:
|
|
206
|
+
break
|
|
207
|
+
except ValueError:
|
|
208
|
+
pass
|
|
209
|
+
else:
|
|
210
|
+
records = []
|
|
211
|
+
for index in range(start, stop, step):
|
|
212
|
+
self.seek(index)
|
|
213
|
+
try:
|
|
214
|
+
records.append(self.read_structure())
|
|
215
|
+
except EOFError:
|
|
216
|
+
break
|
|
217
|
+
except ValueError:
|
|
218
|
+
pass
|
|
219
|
+
return records
|
|
220
|
+
else:
|
|
221
|
+
raise TypeError('Indices must be integers or slices')
|
|
222
|
+
raise NotImplementedError('Indexable supported in unix-like o.s. and for files stored on disk')
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
__all__ = ['MDLRead']
|