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
chython/files/PDBrw.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
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 fileinput import FileInput
|
|
20
|
+
from io import StringIO, TextIOWrapper
|
|
21
|
+
from itertools import islice
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Optional, Sequence, Iterator, List
|
|
24
|
+
from .xyz import xyz
|
|
25
|
+
from ..containers import MoleculeContainer
|
|
26
|
+
from ..exceptions import BufferOverflow
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
one_symbol_names = {'ALA', 'CYS', 'ASP', 'GLU', 'PHE', 'GLY', 'HIS', 'ILE', 'LYS', 'LEU', 'MET', 'ASN', 'PRO', 'GLN',
|
|
30
|
+
'ARG', 'SER', 'THR', 'VAL', 'TRP', 'TYR', # Amino acids
|
|
31
|
+
'ASH', # H-asparagine
|
|
32
|
+
'HID', 'HIE', 'HIP', # H-histidines
|
|
33
|
+
'DA', 'DC', 'DG', 'DT', 'DI', # Deoxyribonucleotides
|
|
34
|
+
'A', 'C', 'G', 'U', 'I', # Ribonucleotides
|
|
35
|
+
'IOD', 'K'} # Elements
|
|
36
|
+
two_symbol_names = {'CL', 'BR', 'NA', 'CA', 'MG', 'CO', 'MN', 'FE', 'CU', 'ZN'}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PDBRead:
|
|
40
|
+
"""PDB files reader. Works similar to opened file object. Support `with` context manager.
|
|
41
|
+
On initialization accept opened in text mode file, string path to file,
|
|
42
|
+
pathlib.Path object or another buffered reader object.
|
|
43
|
+
|
|
44
|
+
Supported multiple structures in same file separated by ENDMDL. Supported only ATOM and HETATM parsing.
|
|
45
|
+
END or ENDMDL required in the end.
|
|
46
|
+
"""
|
|
47
|
+
molecule_cls = MoleculeContainer
|
|
48
|
+
|
|
49
|
+
def __init__(self, file, *, buffer_size=10000, ignore: bool = True, element_name_priority: bool = False,
|
|
50
|
+
parse_as_single: bool = False, atom_name_map=None, charge_map: Optional[Sequence[int]] = None,
|
|
51
|
+
radical_map: Optional[Sequence[int]] = None, radius_multiplier: float = 1.25):
|
|
52
|
+
"""
|
|
53
|
+
:param ignore: Skip some checks of data or try to fix some errors.
|
|
54
|
+
:param element_name_priority: For ligands use element symbol column value and ignore atom name column.
|
|
55
|
+
:param parse_as_single: Usable if all models in file is the same structure.
|
|
56
|
+
2d graph will be restored only from first model.
|
|
57
|
+
:param atom_name_map: dictionary with atom names replacements. e.g.: {'Ow': 'O'}. Keys should be capitalized.
|
|
58
|
+
:param charge_map: iterable with total charges of each model in file.
|
|
59
|
+
:param radical_map: iterable with total radicals count of each model in file.
|
|
60
|
+
"""
|
|
61
|
+
if isinstance(file, str):
|
|
62
|
+
self.__file = open(file)
|
|
63
|
+
self.__is_buffer = False
|
|
64
|
+
elif isinstance(file, Path):
|
|
65
|
+
self.__file = file.open()
|
|
66
|
+
self.__is_buffer = False
|
|
67
|
+
elif isinstance(file, (TextIOWrapper, StringIO, FileInput)):
|
|
68
|
+
self.__file = file
|
|
69
|
+
self.__is_buffer = True
|
|
70
|
+
else:
|
|
71
|
+
raise TypeError('invalid file. TextIOWrapper, StringIO subclasses expected')
|
|
72
|
+
|
|
73
|
+
self.__radius_multiplier = radius_multiplier
|
|
74
|
+
self.__ignore = ignore
|
|
75
|
+
self.__element_name_priority = element_name_priority
|
|
76
|
+
self.__parse_as_single = parse_as_single
|
|
77
|
+
self.__parsed_first = None
|
|
78
|
+
self.__atom_name_map = atom_name_map or {}
|
|
79
|
+
self.__charge_map = charge_map
|
|
80
|
+
self.__radical_map = radical_map
|
|
81
|
+
|
|
82
|
+
self.__buffer = None
|
|
83
|
+
self.__buffer_size = buffer_size
|
|
84
|
+
self.__tell = 0
|
|
85
|
+
|
|
86
|
+
def read(self, amount: Optional[int] = None) -> List[MoleculeContainer]:
|
|
87
|
+
"""
|
|
88
|
+
Parse whole file
|
|
89
|
+
|
|
90
|
+
:param amount: number of records to read
|
|
91
|
+
"""
|
|
92
|
+
if amount:
|
|
93
|
+
return list(islice(iter(self), amount))
|
|
94
|
+
return list(iter(self))
|
|
95
|
+
|
|
96
|
+
def read_structure(self, *, current: bool = True) -> MoleculeContainer:
|
|
97
|
+
"""
|
|
98
|
+
Read Molecule container.
|
|
99
|
+
|
|
100
|
+
:param current: return current structure if already parsed, otherwise read next
|
|
101
|
+
"""
|
|
102
|
+
data = self._read_block(current=current)
|
|
103
|
+
element_name_priority = self.__element_name_priority
|
|
104
|
+
atom_name_map = self.__atom_name_map
|
|
105
|
+
|
|
106
|
+
atoms = []
|
|
107
|
+
res = []
|
|
108
|
+
charges = []
|
|
109
|
+
log = []
|
|
110
|
+
for line in data:
|
|
111
|
+
if line.startswith(('ATOM', 'HETATM')):
|
|
112
|
+
# COLUMNS DATA TYPE FIELD DEFINITION
|
|
113
|
+
# -------------------------------------------------------------------------------------
|
|
114
|
+
# 1 - 6 Record name "ATOM " or "HETATM"
|
|
115
|
+
# 7 - 11 Integer serial Atom serial number.
|
|
116
|
+
# 13 - 16 Atom name Atom name.
|
|
117
|
+
# 17 Character altLoc Alternate location indicator.
|
|
118
|
+
# 18 - 20 Residue name resName Residue name.
|
|
119
|
+
# 22 Character chainID Chain identifier.
|
|
120
|
+
# 23 - 26 Integer resSeq Residue sequence number.
|
|
121
|
+
# 27 AChar iCode Code for insertion of residues.
|
|
122
|
+
# 31 - 38 Real(8.3) x Orthogonal coordinates for X in Angstroms.
|
|
123
|
+
# 39 - 46 Real(8.3) y Orthogonal coordinates for Y in Angstroms.
|
|
124
|
+
# 47 - 54 Real(8.3) z Orthogonal coordinates for Z in Angstroms.
|
|
125
|
+
# 55 - 60 Real(6.2) occupancy Occupancy.
|
|
126
|
+
# 61 - 66 Real(6.2) tempFactor Temperature factor.
|
|
127
|
+
# 77 - 78 LString(2) element Element symbol, right-justified.
|
|
128
|
+
# 79 - 80 LString(2) charge Charge on the atom.
|
|
129
|
+
charge = line[78:80].strip()
|
|
130
|
+
if charge:
|
|
131
|
+
charge = int(charge)
|
|
132
|
+
else:
|
|
133
|
+
charge = None
|
|
134
|
+
x, y, z = float(line[30:38]), float(line[38:46]), float(line[46:54])
|
|
135
|
+
element = line[76:78].strip()
|
|
136
|
+
residue = line[17:20].strip()
|
|
137
|
+
atom_name = line[12:16].strip(' 0123456789-+')
|
|
138
|
+
if residue in one_symbol_names: # bio-polymers and I
|
|
139
|
+
atom_name = atom_name[0]
|
|
140
|
+
elif residue in two_symbol_names:
|
|
141
|
+
atom_name = atom_name[:2].capitalize()
|
|
142
|
+
elif residue == 'MSE':
|
|
143
|
+
if atom_name.startswith('SE'):
|
|
144
|
+
atom_name = 'Se'
|
|
145
|
+
else:
|
|
146
|
+
atom_name = atom_name[0]
|
|
147
|
+
elif residue == 'CBR':
|
|
148
|
+
if atom_name.startswith('BR'):
|
|
149
|
+
atom_name = 'Br'
|
|
150
|
+
else:
|
|
151
|
+
atom_name = atom_name[0]
|
|
152
|
+
# ligands
|
|
153
|
+
elif element_name_priority:
|
|
154
|
+
atom_name = element
|
|
155
|
+
else:
|
|
156
|
+
atom_name = atom_name.capitalize()
|
|
157
|
+
atom_name = atom_name_map.get(atom_name, atom_name)
|
|
158
|
+
|
|
159
|
+
if atom_name != element:
|
|
160
|
+
log.append(f'Atom name and Element symbol is not equal: {line[:-1]}')
|
|
161
|
+
if not self.__ignore:
|
|
162
|
+
raise ValueError('Atom name and Element symbol is not equal')
|
|
163
|
+
atoms.append((atom_name, x, y, z))
|
|
164
|
+
res.append(residue)
|
|
165
|
+
charges.append(charge)
|
|
166
|
+
|
|
167
|
+
if not atoms:
|
|
168
|
+
raise ValueError('invalid PDB')
|
|
169
|
+
|
|
170
|
+
if self.__charge_map:
|
|
171
|
+
t_charge = self.__charge_map[self.__tell - 1]
|
|
172
|
+
radical = self.__radical_map[self.__tell - 1]
|
|
173
|
+
else:
|
|
174
|
+
t_charge = radical = 0
|
|
175
|
+
|
|
176
|
+
if self.__parsed_first is None:
|
|
177
|
+
mol = xyz(atoms, charge=t_charge, radical=radical, radius_multiplier=self.__radius_multiplier,
|
|
178
|
+
atom_charge=charges, _cls=self.molecule_cls)
|
|
179
|
+
|
|
180
|
+
mol.meta['RESIDUE'] = dict(enumerate(res, 1))
|
|
181
|
+
if log:
|
|
182
|
+
mol.meta['chython_parsing_log'] = log
|
|
183
|
+
if self.__parse_as_single:
|
|
184
|
+
self.__parsed_first = mol.copy()
|
|
185
|
+
return mol
|
|
186
|
+
else:
|
|
187
|
+
if len(self.__parsed_first) != len(atoms):
|
|
188
|
+
raise ValueError('models not equal')
|
|
189
|
+
c = {}
|
|
190
|
+
for (n, a), (e, x, y, z) in zip(self.__parsed_first.atoms(), atoms):
|
|
191
|
+
if a.atomic_symbol != e:
|
|
192
|
+
raise ValueError('models or atom order not equal')
|
|
193
|
+
c[n] = (x, y, z)
|
|
194
|
+
mol = self.__parsed_first.copy()
|
|
195
|
+
mol._conformers[0] = c
|
|
196
|
+
if log:
|
|
197
|
+
if 'chython_parsing_log' in mol.meta:
|
|
198
|
+
mol.meta['chython_parsing_log'] = mol.meta['chython_parsing_log'] + log
|
|
199
|
+
else:
|
|
200
|
+
mol.meta['chython_parsing_log'] = log
|
|
201
|
+
return mol
|
|
202
|
+
|
|
203
|
+
def close(self, force: bool = False):
|
|
204
|
+
"""
|
|
205
|
+
Close opened file
|
|
206
|
+
|
|
207
|
+
:param force: force closing of externally opened file or buffer
|
|
208
|
+
"""
|
|
209
|
+
if not self.__is_buffer or force:
|
|
210
|
+
self.__file.close()
|
|
211
|
+
|
|
212
|
+
def tell(self):
|
|
213
|
+
"""
|
|
214
|
+
Number of records processed from the original file
|
|
215
|
+
"""
|
|
216
|
+
return self.__tell
|
|
217
|
+
|
|
218
|
+
def __enter__(self):
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
def __exit__(self, _type, value, traceback):
|
|
222
|
+
self.close()
|
|
223
|
+
|
|
224
|
+
def __iter__(self) -> Iterator[MoleculeContainer]:
|
|
225
|
+
while True:
|
|
226
|
+
try:
|
|
227
|
+
yield self.read_structure(current=False)
|
|
228
|
+
except ValueError:
|
|
229
|
+
pass
|
|
230
|
+
except EOFError:
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
def __next__(self) -> MoleculeContainer:
|
|
234
|
+
return next(iter(self))
|
|
235
|
+
|
|
236
|
+
def _read_block(self, *, current=True) -> List[str]:
|
|
237
|
+
if current and self.__buffer:
|
|
238
|
+
return self.__buffer
|
|
239
|
+
self.__buffer = None
|
|
240
|
+
buffer_size = self.__buffer_size
|
|
241
|
+
buffer = []
|
|
242
|
+
|
|
243
|
+
for n, line in enumerate(self.__file):
|
|
244
|
+
buffer.append(line)
|
|
245
|
+
if line.startswith('END'):
|
|
246
|
+
break
|
|
247
|
+
elif n == buffer_size:
|
|
248
|
+
raise BufferOverflow
|
|
249
|
+
else:
|
|
250
|
+
raise EOFError
|
|
251
|
+
|
|
252
|
+
self.__tell += 1
|
|
253
|
+
self.__buffer = buffer
|
|
254
|
+
return buffer
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
__all__ = ['PDBRead']
|
chython/files/RDFrw.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2014-2024 Ramil Nugmanov <nougmanoff@protonmail.com>
|
|
4
|
+
# Copyright 2019 Dinar Batyrshin <batyrshin-dinar@mail.ru>
|
|
5
|
+
# This file is part of chython.
|
|
6
|
+
#
|
|
7
|
+
# chython is free software; you can redistribute it and/or modify
|
|
8
|
+
# it under the terms of the GNU Lesser General Public License as published by
|
|
9
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
10
|
+
# (at your option) any later version.
|
|
11
|
+
#
|
|
12
|
+
# This program is distributed in the hope that it will be useful,
|
|
13
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
# GNU Lesser General Public License for more details.
|
|
16
|
+
#
|
|
17
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
18
|
+
# along with this program; if not, see <https://www.gnu.org/licenses/>.
|
|
19
|
+
#
|
|
20
|
+
from collections import defaultdict
|
|
21
|
+
from io import BytesIO
|
|
22
|
+
from itertools import chain
|
|
23
|
+
from pickle import dump
|
|
24
|
+
from subprocess import check_output
|
|
25
|
+
from sys import platform
|
|
26
|
+
from time import strftime
|
|
27
|
+
from typing import Union, Dict, List
|
|
28
|
+
from .mdl import (MDLRead, MOLWrite, EMOLWrite, parse_mol_v2000, parse_mol_v3000, parse_rxn_v2000, parse_rxn_v3000,
|
|
29
|
+
postprocess_molecule)
|
|
30
|
+
from ._convert import create_molecule, create_reaction
|
|
31
|
+
from ._mapping import postprocess_parsed_molecule, postprocess_parsed_reaction
|
|
32
|
+
from ..containers import ReactionContainer, MoleculeContainer
|
|
33
|
+
from ..exceptions import BufferOverflow
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RDFRead(MDLRead):
|
|
37
|
+
"""
|
|
38
|
+
MDL RDF files reader. works similar to opened file object. support `with` context manager.
|
|
39
|
+
on initialization accept opened in text mode file, string path to file,
|
|
40
|
+
pathlib.Path object or another buffered reader object
|
|
41
|
+
"""
|
|
42
|
+
molecule_cls = MoleculeContainer
|
|
43
|
+
reaction_cls = ReactionContainer
|
|
44
|
+
|
|
45
|
+
def __init__(self, file, *, buffer_size=10000, indexable: bool = False, ignore: bool = True, remap: bool = False,
|
|
46
|
+
calc_cis_trans: bool = False, ignore_stereo: bool = False, ignore_bad_isotopes: bool = False):
|
|
47
|
+
"""
|
|
48
|
+
:param buffer_size: readahead size. increase if you have big molecules or metadata records.
|
|
49
|
+
:param indexable: if True: supported methods seek, tell, object size and subscription, it only works when
|
|
50
|
+
dealing with a real file (the path to the file is specified) because the external grep utility is used,
|
|
51
|
+
supporting in unix-like OS the object behaves like a normal open file.
|
|
52
|
+
|
|
53
|
+
if False: works like generator converting a record into MoleculeContainer and returning each object in
|
|
54
|
+
order, records with errors are skipped
|
|
55
|
+
:param ignore: Skip some checks of data or try to fix some errors.
|
|
56
|
+
:param remap: Remap atom numbers started from one.
|
|
57
|
+
:param calc_cis_trans: Calculate cis/trans marks from 2d coordinates.
|
|
58
|
+
:param ignore_stereo: Ignore stereo data.
|
|
59
|
+
:param ignore_bad_isotopes: reset invalid isotope mark to non-isotopic.
|
|
60
|
+
"""
|
|
61
|
+
super().__init__(file, indexable=indexable, ignore=ignore, remap=remap, ignore_bad_isotopes=ignore_bad_isotopes,
|
|
62
|
+
ignore_stereo=ignore_stereo, calc_cis_trans=calc_cis_trans, buffer_size=buffer_size)
|
|
63
|
+
self.__m_start = None
|
|
64
|
+
|
|
65
|
+
def read_structure(self, *, current=True) -> Union[ReactionContainer, MoleculeContainer]:
|
|
66
|
+
data = self._read_block(current=current)
|
|
67
|
+
meta = self.read_metadata()
|
|
68
|
+
if data[0].startswith('$RXN'):
|
|
69
|
+
if data[4].startswith('M V30 COUNTS'):
|
|
70
|
+
tmp = parse_rxn_v3000(data, ignore=self._ignore)
|
|
71
|
+
else:
|
|
72
|
+
tmp = parse_rxn_v2000(data, ignore=self._ignore)
|
|
73
|
+
|
|
74
|
+
postprocess_parsed_reaction(tmp, remap=self._remap, ignore=self._ignore)
|
|
75
|
+
rxn = create_reaction(tmp, ignore_bad_isotopes=self._ignore_bad_isotopes, _m_cls=self.molecule_cls,
|
|
76
|
+
_r_cls=self.reaction_cls)
|
|
77
|
+
if not self._ignore_stereo:
|
|
78
|
+
for mol, tmp in zip(rxn.molecules(), chain(tmp['reactants'], tmp['reagents'], tmp['products'])):
|
|
79
|
+
postprocess_molecule(mol, tmp, calc_cis_trans=self._calc_cis_trans)
|
|
80
|
+
if meta:
|
|
81
|
+
rxn.meta.update(meta)
|
|
82
|
+
return rxn
|
|
83
|
+
elif data[4].startswith('M V30 BEGIN CTAB'):
|
|
84
|
+
tmp = parse_mol_v3000(data)
|
|
85
|
+
else:
|
|
86
|
+
tmp = parse_mol_v2000(data)
|
|
87
|
+
|
|
88
|
+
postprocess_parsed_molecule(tmp)
|
|
89
|
+
mol = create_molecule(tmp, ignore_bad_isotopes=self._ignore_bad_isotopes, _cls=self.molecule_cls)
|
|
90
|
+
if not self._ignore_stereo:
|
|
91
|
+
postprocess_molecule(mol, tmp, calc_cis_trans=self._calc_cis_trans)
|
|
92
|
+
if meta:
|
|
93
|
+
mol.meta.update(meta)
|
|
94
|
+
return mol
|
|
95
|
+
|
|
96
|
+
def read_metadata(self, *, current=True) -> Dict[str, str]:
|
|
97
|
+
mkey = None
|
|
98
|
+
meta = defaultdict(list)
|
|
99
|
+
for line in self._read_metadata(current=current):
|
|
100
|
+
if line.startswith('$DTYPE'):
|
|
101
|
+
mkey = line[7:].strip()
|
|
102
|
+
if not mkey:
|
|
103
|
+
meta['chython_unparsed_metadata'].append(line.strip())
|
|
104
|
+
elif mkey:
|
|
105
|
+
data = line.lstrip("$DATUM").strip()
|
|
106
|
+
if data:
|
|
107
|
+
meta[mkey].append(data)
|
|
108
|
+
else:
|
|
109
|
+
meta['chython_unparsed_metadata'].append(line.strip())
|
|
110
|
+
return {k: '\n'.join(v) for k, v in meta.items()}
|
|
111
|
+
|
|
112
|
+
def read_rxn(self, *, current: bool = True) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Read rxn block without metadata
|
|
115
|
+
"""
|
|
116
|
+
return ''.join(self._read_block(current=current)[:self.__m_start])
|
|
117
|
+
|
|
118
|
+
def read_mol(self, n: int, /, *, current: bool = True) -> str:
|
|
119
|
+
"""
|
|
120
|
+
Read requested MOL block
|
|
121
|
+
"""
|
|
122
|
+
data = self._read_block(current=current)
|
|
123
|
+
if data[0].startswith('$RXN'):
|
|
124
|
+
if data[4].startswith('M V30 COUNTS'):
|
|
125
|
+
idx_l = [i for i, x in enumerate(data) if x.startswith('M V30 BEGIN CTAB')]
|
|
126
|
+
idx_r = [i for i, x in enumerate(data, 1) if x.startswith('M V30 END CTAB')]
|
|
127
|
+
if 0 <= n < len(idx_l):
|
|
128
|
+
ct = ''.join(data[idx_l[n]: idx_r[n]])
|
|
129
|
+
return f'\n\n\n 0 0 0 0 0 999 V3000\n{ct}M END\n'
|
|
130
|
+
else:
|
|
131
|
+
raise IndexError('molecule number is out of range')
|
|
132
|
+
else:
|
|
133
|
+
idx = [i for i, x in enumerate(data) if x.startswith('$MOL')]
|
|
134
|
+
if 0 <= n < len(idx):
|
|
135
|
+
idx.append(self.__m_start)
|
|
136
|
+
return ''.join(data[idx[n] + 1: idx[n + 1]])
|
|
137
|
+
else:
|
|
138
|
+
raise IndexError('molecule number is out of range')
|
|
139
|
+
elif n:
|
|
140
|
+
raise IndexError('molecule number is out of range')
|
|
141
|
+
return ''.join(data[:self.__m_start])
|
|
142
|
+
|
|
143
|
+
def seek(self, offset):
|
|
144
|
+
super().seek(offset)
|
|
145
|
+
self.__m_start = None
|
|
146
|
+
|
|
147
|
+
def reset_index(self):
|
|
148
|
+
if platform != 'win32' and not self._is_buffer:
|
|
149
|
+
shifts = []
|
|
150
|
+
for x in BytesIO(check_output(['grep', '-bE', r'^\$[RM]FMT', self._file.name])):
|
|
151
|
+
pos, _ = x.split(b':', 1)
|
|
152
|
+
shifts.append(int(pos))
|
|
153
|
+
shifts[0] = 0 # first record parsing always starts from the beginning
|
|
154
|
+
with open(self._cache_path, 'wb') as f:
|
|
155
|
+
dump(shifts, f)
|
|
156
|
+
self._shifts = shifts
|
|
157
|
+
else:
|
|
158
|
+
raise NotImplementedError('Indexable supported in unix-like o.s. and for files stored on disk')
|
|
159
|
+
|
|
160
|
+
def _read_block(self, *, current=True) -> List[str]:
|
|
161
|
+
"""
|
|
162
|
+
Read RXN or MOL block with metadata
|
|
163
|
+
"""
|
|
164
|
+
if current and self._buffer:
|
|
165
|
+
return self._buffer
|
|
166
|
+
self.__m_start = m_start = None
|
|
167
|
+
self._buffer = buffer = []
|
|
168
|
+
buffer_size = self._buffer_size
|
|
169
|
+
|
|
170
|
+
drop = not self._tell # only first record starts from [RM]FMT search
|
|
171
|
+
for n, line in enumerate(self._file):
|
|
172
|
+
if drop:
|
|
173
|
+
if line.startswith('$RXN'): # RXN file
|
|
174
|
+
drop = False
|
|
175
|
+
buffer.append(line)
|
|
176
|
+
elif line.startswith(('$RFMT', '$MFMT')): # first occurrence found
|
|
177
|
+
drop = False
|
|
178
|
+
continue
|
|
179
|
+
elif n == buffer_size:
|
|
180
|
+
raise BufferOverflow
|
|
181
|
+
elif not m_start and line.startswith('$DTYPE'):
|
|
182
|
+
self.__m_start = m_start = len(buffer)
|
|
183
|
+
elif line.startswith(('$RFMT', '$MFMT')): # next record found
|
|
184
|
+
break
|
|
185
|
+
buffer.append(line)
|
|
186
|
+
if buffer:
|
|
187
|
+
self._tell += 1
|
|
188
|
+
else:
|
|
189
|
+
raise EOFError
|
|
190
|
+
return buffer
|
|
191
|
+
|
|
192
|
+
def _read_metadata(self, *, current: bool = True):
|
|
193
|
+
data = self._read_block(current=current)
|
|
194
|
+
if not self.__m_start:
|
|
195
|
+
return []
|
|
196
|
+
return data[self.__m_start:]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class _RDFWrite:
|
|
200
|
+
def __init__(self, file, *, append: bool = False, mapping: bool = True):
|
|
201
|
+
"""
|
|
202
|
+
:param append: append to existing file (True) or rewrite it (False). For buffered writer object append = False
|
|
203
|
+
will write RDF header and append = True will omit the header.
|
|
204
|
+
:param mapping: write atom mapping.
|
|
205
|
+
"""
|
|
206
|
+
super().__init__(file, append=append, mapping=mapping)
|
|
207
|
+
if not append or not (self._is_buffer or self._file.tell() != 0):
|
|
208
|
+
self.write = self.__write
|
|
209
|
+
|
|
210
|
+
def __write(self, data):
|
|
211
|
+
"""
|
|
212
|
+
write single molecule or reaction into file
|
|
213
|
+
"""
|
|
214
|
+
del self.write
|
|
215
|
+
self._file.write(strftime('$RDFILE 1\n$DATM %m/%d/%y %H:%M\n'))
|
|
216
|
+
self.write(data)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class RDFWrite(_RDFWrite, MOLWrite):
|
|
220
|
+
"""
|
|
221
|
+
MDL RDF files writer. works similar to opened for writing file object. support `with` context manager.
|
|
222
|
+
on initialization accept opened for writing in text mode file, string path to file,
|
|
223
|
+
pathlib.Path object or another buffered writer object
|
|
224
|
+
"""
|
|
225
|
+
def write(self, data: Union[ReactionContainer, MoleculeContainer]):
|
|
226
|
+
file = self._file
|
|
227
|
+
if isinstance(data, ReactionContainer):
|
|
228
|
+
file.write(f'$RFMT\n$RXN\n{data.name}\n\n\n{len(data.reactants):3d}{len(data.products):3d}')
|
|
229
|
+
if data.reagents:
|
|
230
|
+
file.write(f'{len(data.reagents):3d}\n')
|
|
231
|
+
else:
|
|
232
|
+
file.write('\n')
|
|
233
|
+
for m in chain(data.reactants, data.products, data.reagents):
|
|
234
|
+
file.write('$MOL\n')
|
|
235
|
+
self._write_molecule(m)
|
|
236
|
+
else:
|
|
237
|
+
file.write('$MFMT\n')
|
|
238
|
+
self._write_molecule(data)
|
|
239
|
+
for k, v in data.meta.items():
|
|
240
|
+
file.write(f'$DTYPE {k}\n$DATUM {v}\n')
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class ERDFWrite(_RDFWrite, EMOLWrite):
|
|
244
|
+
"""
|
|
245
|
+
MDL V3000 RDF files writer. works similar to opened for writing file object. support `with` context manager.
|
|
246
|
+
on initialization accept opened for writing in text mode file, string path to file,
|
|
247
|
+
pathlib.Path object or another buffered writer object
|
|
248
|
+
"""
|
|
249
|
+
def write(self, data: Union[ReactionContainer, MoleculeContainer]):
|
|
250
|
+
file = self._file
|
|
251
|
+
if isinstance(data, ReactionContainer):
|
|
252
|
+
file.write(f'$RFMT\n$RXN V3000\n{data.name}\n\n\nM V30 COUNTS {len(data.reactants)} {len(data.products)}')
|
|
253
|
+
if data.reagents:
|
|
254
|
+
file.write(f' {len(data.reagents)}\nM V30 BEGIN REACTANT\n')
|
|
255
|
+
else:
|
|
256
|
+
file.write('\nM V30 BEGIN REACTANT\n')
|
|
257
|
+
for m in data.reactants:
|
|
258
|
+
self._write_molecule(m)
|
|
259
|
+
file.write('M V30 END REACTANT\nM V30 BEGIN PRODUCT\n')
|
|
260
|
+
for m in data.products:
|
|
261
|
+
self._write_molecule(m)
|
|
262
|
+
file.write('M V30 END PRODUCT\n')
|
|
263
|
+
if data.reagents:
|
|
264
|
+
file.write('M V30 BEGIN AGENT\n')
|
|
265
|
+
for m in data.reagents:
|
|
266
|
+
self._write_molecule(m)
|
|
267
|
+
file.write('M V30 END AGENT\n')
|
|
268
|
+
file.write('M END\n')
|
|
269
|
+
else:
|
|
270
|
+
file.write(f'$MFMT\n{data.name}\n\n\n 0 0 0 0 0 999 V3000\n')
|
|
271
|
+
self._write_molecule(data)
|
|
272
|
+
file.write('M END\n')
|
|
273
|
+
for k, v in data.meta.items():
|
|
274
|
+
file.write(f'$DTYPE {k}\n$DATUM {v}\n')
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def mdl_rxn(data: str, /, *, ignore=True, calc_cis_trans=False, ignore_stereo=False, remap=False,
|
|
278
|
+
ignore_bad_isotopes=False, _r_cls=ReactionContainer, _m_cls=MoleculeContainer) -> ReactionContainer:
|
|
279
|
+
"""
|
|
280
|
+
Parse string with rxn file.
|
|
281
|
+
"""
|
|
282
|
+
data = data.splitlines()
|
|
283
|
+
if not data[0].startswith('$RXN'):
|
|
284
|
+
raise ValueError('invalid RXN')
|
|
285
|
+
if data[4].startswith('M V30 COUNTS'):
|
|
286
|
+
tmp = parse_rxn_v3000(data, ignore=ignore)
|
|
287
|
+
else:
|
|
288
|
+
tmp = parse_rxn_v2000(data, ignore=ignore)
|
|
289
|
+
|
|
290
|
+
postprocess_parsed_reaction(tmp, remap=remap, ignore=ignore)
|
|
291
|
+
rxn = create_reaction(tmp, ignore_bad_isotopes=ignore_bad_isotopes, _m_cls=_m_cls, _r_cls=_r_cls)
|
|
292
|
+
if not ignore_stereo:
|
|
293
|
+
for mol, tmp in zip(rxn.molecules(), chain(tmp['reactants'], tmp['reagents'], tmp['products'])):
|
|
294
|
+
postprocess_molecule(mol, tmp, calc_cis_trans=calc_cis_trans)
|
|
295
|
+
return rxn
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
__all__ = ['RDFRead', 'RDFWrite', 'ERDFWrite', 'mdl_rxn']
|