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.
Files changed (171) hide show
  1. chython/__init__.py +31 -0
  2. chython/_functions.py +69 -0
  3. chython/algorithms/__init__.py +18 -0
  4. chython/algorithms/_isomorphism.cp312-win_amd64.pyd +0 -0
  5. chython/algorithms/_isomorphism.pyx +189 -0
  6. chython/algorithms/aromatics/__init__.py +27 -0
  7. chython/algorithms/aromatics/_rules.py +124 -0
  8. chython/algorithms/aromatics/kekule.py +530 -0
  9. chython/algorithms/aromatics/test/__init__.py +18 -0
  10. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  11. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  12. chython/algorithms/aromatics/thiele.py +226 -0
  13. chython/algorithms/calculate2d/__init__.py +23 -0
  14. chython/algorithms/calculate2d/clean2d.js +1 -0
  15. chython/algorithms/calculate2d/molecule.py +149 -0
  16. chython/algorithms/calculate2d/reaction.py +80 -0
  17. chython/algorithms/depict.py +523 -0
  18. chython/algorithms/fingerprints/__init__.py +48 -0
  19. chython/algorithms/fingerprints/linear.py +208 -0
  20. chython/algorithms/fingerprints/morgan.py +137 -0
  21. chython/algorithms/fingerprints/test/__init__.py +18 -0
  22. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  23. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  24. chython/algorithms/isomorphism.py +667 -0
  25. chython/algorithms/mapping/__init__.py +28 -0
  26. chython/algorithms/mapping/_groups.py +94 -0
  27. chython/algorithms/mapping/_reactions.py +72 -0
  28. chython/algorithms/mapping/attention.py +216 -0
  29. chython/algorithms/mapping/fixmapper.py +84 -0
  30. chython/algorithms/mapping/groups.py +137 -0
  31. chython/algorithms/mcs.py +202 -0
  32. chython/algorithms/morgan.py +82 -0
  33. chython/algorithms/rings.py +551 -0
  34. chython/algorithms/smiles.py +556 -0
  35. chython/algorithms/standardize/__init__.py +30 -0
  36. chython/algorithms/standardize/_charged.py +115 -0
  37. chython/algorithms/standardize/_groups.py +926 -0
  38. chython/algorithms/standardize/_metal_organics.py +189 -0
  39. chython/algorithms/standardize/_reagents.py +44 -0
  40. chython/algorithms/standardize/_salts.py +68 -0
  41. chython/algorithms/standardize/molecule.py +479 -0
  42. chython/algorithms/standardize/reaction.py +429 -0
  43. chython/algorithms/standardize/resonance.py +196 -0
  44. chython/algorithms/standardize/salts.py +128 -0
  45. chython/algorithms/standardize/saturation.py +378 -0
  46. chython/algorithms/standardize/test/__init__.py +18 -0
  47. chython/algorithms/standardize/test/test_groups.py +118 -0
  48. chython/algorithms/stereo.py +1222 -0
  49. chython/algorithms/tautomers/__init__.py +171 -0
  50. chython/algorithms/tautomers/_acid.py +59 -0
  51. chython/algorithms/tautomers/_base.py +116 -0
  52. chython/algorithms/tautomers/_keto_enol.py +107 -0
  53. chython/algorithms/tautomers/acid_base.py +214 -0
  54. chython/algorithms/tautomers/heteroarenes.py +108 -0
  55. chython/algorithms/tautomers/keto_enol.py +148 -0
  56. chython/algorithms/tautomers/test/__init__.py +18 -0
  57. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  58. chython/algorithms/test/__init__.py +18 -0
  59. chython/algorithms/test/test_isomorphism.py +58 -0
  60. chython/algorithms/test/test_smiles.py +76 -0
  61. chython/algorithms/x3dom.py +355 -0
  62. chython/containers/__init__.py +43 -0
  63. chython/containers/_pack_v2.cp312-win_amd64.pyd +0 -0
  64. chython/containers/_pack_v2.pyx +271 -0
  65. chython/containers/_unpack_v0v2.cp312-win_amd64.pyd +0 -0
  66. chython/containers/_unpack_v0v2.pyx +300 -0
  67. chython/containers/bonds.py +255 -0
  68. chython/containers/cgr.py +115 -0
  69. chython/containers/graph.py +200 -0
  70. chython/containers/molecule.py +695 -0
  71. chython/containers/query.py +66 -0
  72. chython/containers/reaction.py +324 -0
  73. chython/exceptions.py +114 -0
  74. chython/files/MRVrw.py +523 -0
  75. chython/files/PDBrw.py +257 -0
  76. chython/files/RDFrw.py +298 -0
  77. chython/files/SDFrw.py +221 -0
  78. chython/files/__init__.py +29 -0
  79. chython/files/_convert.py +207 -0
  80. chython/files/_mapping.py +117 -0
  81. chython/files/_xyz.cp312-win_amd64.pyd +0 -0
  82. chython/files/_xyz.pyx +74 -0
  83. chython/files/daylight/__init__.py +24 -0
  84. chython/files/daylight/parser.py +157 -0
  85. chython/files/daylight/smarts.py +107 -0
  86. chython/files/daylight/smiles.py +245 -0
  87. chython/files/daylight/test/__init__.py +18 -0
  88. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  89. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  90. chython/files/daylight/test/test_parser.py +118 -0
  91. chython/files/daylight/test/test_tokenize.py +77 -0
  92. chython/files/daylight/tokenize.py +376 -0
  93. chython/files/libinchi/__init__.py +22 -0
  94. chython/files/libinchi/libinchi.dll +0 -0
  95. chython/files/libinchi/wrapper.py +555 -0
  96. chython/files/mdl/__init__.py +25 -0
  97. chython/files/mdl/emol.py +209 -0
  98. chython/files/mdl/erxn.py +67 -0
  99. chython/files/mdl/mol.py +144 -0
  100. chython/files/mdl/read.py +225 -0
  101. chython/files/mdl/rxn.py +67 -0
  102. chython/files/mdl/stereo.py +63 -0
  103. chython/files/mdl/write.py +165 -0
  104. chython/files/xyz.py +78 -0
  105. chython/periodictable/__init__.py +69 -0
  106. chython/periodictable/base/__init__.py +25 -0
  107. chython/periodictable/base/dynamic.py +149 -0
  108. chython/periodictable/base/element.py +452 -0
  109. chython/periodictable/base/groups.py +90 -0
  110. chython/periodictable/base/periods.py +46 -0
  111. chython/periodictable/base/query.py +473 -0
  112. chython/periodictable/base/vector.py +119 -0
  113. chython/periodictable/groupI.py +252 -0
  114. chython/periodictable/groupII.py +223 -0
  115. chython/periodictable/groupIII.py +1154 -0
  116. chython/periodictable/groupIV.py +210 -0
  117. chython/periodictable/groupIX.py +196 -0
  118. chython/periodictable/groupV.py +192 -0
  119. chython/periodictable/groupVI.py +183 -0
  120. chython/periodictable/groupVII.py +162 -0
  121. chython/periodictable/groupVIII.py +161 -0
  122. chython/periodictable/groupX.py +166 -0
  123. chython/periodictable/groupXI.py +164 -0
  124. chython/periodictable/groupXII.py +158 -0
  125. chython/periodictable/groupXIII.py +239 -0
  126. chython/periodictable/groupXIV.py +262 -0
  127. chython/periodictable/groupXV.py +276 -0
  128. chython/periodictable/groupXVI.py +457 -0
  129. chython/periodictable/groupXVII.py +324 -0
  130. chython/periodictable/groupXVIII.py +260 -0
  131. chython/reactor/__init__.py +24 -0
  132. chython/reactor/base.py +212 -0
  133. chython/reactor/deprotection.py +565 -0
  134. chython/reactor/groups.py +128 -0
  135. chython/reactor/reactions/__init__.py +157 -0
  136. chython/reactor/reactions/_amidation.py +58 -0
  137. chython/reactor/reactions/_amine_isocyanate.py +52 -0
  138. chython/reactor/reactions/_buchwald_hartwig.py +50 -0
  139. chython/reactor/reactions/_esterification.py +49 -0
  140. chython/reactor/reactions/_macmillan.py +43 -0
  141. chython/reactor/reactions/_reductive_amination.py +50 -0
  142. chython/reactor/reactions/_sonogashira.py +49 -0
  143. chython/reactor/reactions/_sulfonamidation.py +52 -0
  144. chython/reactor/reactions/_suzuki_miyaura.py +88 -0
  145. chython/reactor/reactions/ufe.py +99 -0
  146. chython/reactor/reactor.py +170 -0
  147. chython/reactor/retro/__init__.py +77 -0
  148. chython/reactor/retro/_amidation.py +39 -0
  149. chython/reactor/retro/_aryl_amination.py +32 -0
  150. chython/reactor/retro/_mitsunobu.py +49 -0
  151. chython/reactor/retro/_sonogashira.py +49 -0
  152. chython/reactor/retro/_suzuki_miyaura.py +54 -0
  153. chython/reactor/scaffold.py +122 -0
  154. chython/reactor/test/__init__.py +18 -0
  155. chython/reactor/test/test_deprotection.py +57 -0
  156. chython/reactor/test/test_reactor.py +40 -0
  157. chython/reactor/test/test_scaffold.py +57 -0
  158. chython/reactor/test/test_transformer.py +40 -0
  159. chython/reactor/transformer.py +62 -0
  160. chython/utils/__init__.py +57 -0
  161. chython/utils/free_wilson.py +114 -0
  162. chython/utils/grid.py +130 -0
  163. chython/utils/rdkit.py +165 -0
  164. chython/utils/retro.py +139 -0
  165. chython/utils/svg.py +57 -0
  166. chython/utils/test/__init__.py +18 -0
  167. chython/utils/test/test_rdkit.py +71 -0
  168. chython-2.4.dist-info/LICENSE +165 -0
  169. chython-2.4.dist-info/METADATA +101 -0
  170. chython-2.4.dist-info/RECORD +171 -0
  171. 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']