chython 2.19__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. chython/__init__.py +35 -0
  2. chython/_functions.py +95 -0
  3. chython/_java.py +57 -0
  4. chython/algorithms/__init__.py +18 -0
  5. chython/algorithms/_isomorphism.cp313-win_amd64.pyd +0 -0
  6. chython/algorithms/_isomorphism.pyx +189 -0
  7. chython/algorithms/_rings.cp313-win_amd64.pyd +0 -0
  8. chython/algorithms/_rings.pyx +478 -0
  9. chython/algorithms/aromatics/__init__.py +27 -0
  10. chython/algorithms/aromatics/_rules.py +124 -0
  11. chython/algorithms/aromatics/kekule.py +532 -0
  12. chython/algorithms/aromatics/test/__init__.py +18 -0
  13. chython/algorithms/aromatics/test/test_kekule.py +160 -0
  14. chython/algorithms/aromatics/test/test_thiele.py +263 -0
  15. chython/algorithms/aromatics/thiele.py +222 -0
  16. chython/algorithms/calculate2d/__init__.py +23 -0
  17. chython/algorithms/calculate2d/clean2d.js +1 -0
  18. chython/algorithms/calculate2d/molecule.py +191 -0
  19. chython/algorithms/calculate2d/reaction.py +75 -0
  20. chython/algorithms/conformers.py +106 -0
  21. chython/algorithms/depict.py +596 -0
  22. chython/algorithms/fingerprints/__init__.py +43 -0
  23. chython/algorithms/fingerprints/linear.py +202 -0
  24. chython/algorithms/fingerprints/morgan.py +132 -0
  25. chython/algorithms/fingerprints/test/__init__.py +18 -0
  26. chython/algorithms/fingerprints/test/test_linear.py +127 -0
  27. chython/algorithms/fingerprints/test/test_morgan.py +79 -0
  28. chython/algorithms/groups/__init__.py +212 -0
  29. chython/algorithms/groups/_functional.py +290 -0
  30. chython/algorithms/groups/_oxidations.py +70 -0
  31. chython/algorithms/groups/_protective.py +964 -0
  32. chython/algorithms/groups/_reactions.py +809 -0
  33. chython/algorithms/groups/_reductions.py +88 -0
  34. chython/algorithms/groups/_transformations.py +278 -0
  35. chython/algorithms/groups/test/__init__.py +18 -0
  36. chython/algorithms/groups/test/test_deprotection.py +108 -0
  37. chython/algorithms/groups/test/test_reactions.py +377 -0
  38. chython/algorithms/isomorphism.py +664 -0
  39. chython/algorithms/mapping/__init__.py +188 -0
  40. chython/algorithms/mapping/_groups.py +94 -0
  41. chython/algorithms/mapping/_reactions.py +72 -0
  42. chython/algorithms/mapping/attention.py +266 -0
  43. chython/algorithms/mapping/reconstruct.py +346 -0
  44. chython/algorithms/mcs.py +201 -0
  45. chython/algorithms/morgan.py +77 -0
  46. chython/algorithms/rings.py +178 -0
  47. chython/algorithms/smiles.py +597 -0
  48. chython/algorithms/standardize/__init__.py +30 -0
  49. chython/algorithms/standardize/_groups.py +958 -0
  50. chython/algorithms/standardize/_isomers.py +330 -0
  51. chython/algorithms/standardize/_metal_organics.py +189 -0
  52. chython/algorithms/standardize/_reagents.py +44 -0
  53. chython/algorithms/standardize/_salts.py +68 -0
  54. chython/algorithms/standardize/molecule.py +475 -0
  55. chython/algorithms/standardize/reaction.py +384 -0
  56. chython/algorithms/standardize/resonance.py +191 -0
  57. chython/algorithms/standardize/salts.py +128 -0
  58. chython/algorithms/standardize/saturation.py +373 -0
  59. chython/algorithms/standardize/test/__init__.py +18 -0
  60. chython/algorithms/standardize/test/test_canonicalize.py +66 -0
  61. chython/algorithms/standardize/test/test_groups.py +146 -0
  62. chython/algorithms/standardize/test/test_isomers.py +198 -0
  63. chython/algorithms/stereo.py +1215 -0
  64. chython/algorithms/tautomers/__init__.py +167 -0
  65. chython/algorithms/tautomers/_acid.py +59 -0
  66. chython/algorithms/tautomers/_base.py +116 -0
  67. chython/algorithms/tautomers/_keto_enol.py +107 -0
  68. chython/algorithms/tautomers/acid_base.py +208 -0
  69. chython/algorithms/tautomers/heteroarenes.py +103 -0
  70. chython/algorithms/tautomers/keto_enol.py +143 -0
  71. chython/algorithms/tautomers/test/__init__.py +18 -0
  72. chython/algorithms/tautomers/test/test_tautomers.py +80 -0
  73. chython/algorithms/test/__init__.py +18 -0
  74. chython/algorithms/test/test_isomorphism.py +58 -0
  75. chython/algorithms/test/test_smiles.py +76 -0
  76. chython/algorithms/x3dom.py +350 -0
  77. chython/containers/__init__.py +48 -0
  78. chython/containers/_pack_v2.cp313-win_amd64.pyd +0 -0
  79. chython/containers/_pack_v2.pyx +271 -0
  80. chython/containers/_unpack_v0v2.cp313-win_amd64.pyd +0 -0
  81. chython/containers/_unpack_v0v2.pyx +302 -0
  82. chython/containers/bonds.py +255 -0
  83. chython/containers/cgr.py +115 -0
  84. chython/containers/chimera.py +63 -0
  85. chython/containers/graph.py +199 -0
  86. chython/containers/molecule.py +817 -0
  87. chython/containers/query.py +71 -0
  88. chython/containers/rdkit.py +183 -0
  89. chython/containers/reaction.py +324 -0
  90. chython/containers/test/__init__.py +18 -0
  91. chython/containers/test/test_molecule.py +117 -0
  92. chython/containers/test/test_pack.py +60 -0
  93. chython/containers/test/test_rdkit.py +70 -0
  94. chython/containers/test/test_reaction.py +40 -0
  95. chython/exceptions.py +114 -0
  96. chython/files/MRVrw.py +532 -0
  97. chython/files/PDBrw.py +255 -0
  98. chython/files/RDFrw.py +309 -0
  99. chython/files/SDFrw.py +221 -0
  100. chython/files/__init__.py +30 -0
  101. chython/files/_convert.py +207 -0
  102. chython/files/_mapping.py +117 -0
  103. chython/files/_xyz.cp313-win_amd64.pyd +0 -0
  104. chython/files/_xyz.pyx +74 -0
  105. chython/files/daylight/__init__.py +24 -0
  106. chython/files/daylight/parser.py +157 -0
  107. chython/files/daylight/smarts.py +107 -0
  108. chython/files/daylight/smiles.py +260 -0
  109. chython/files/daylight/test/__init__.py +18 -0
  110. chython/files/daylight/test/test_daylight_smarts.py +289 -0
  111. chython/files/daylight/test/test_daylight_smiles.py +101 -0
  112. chython/files/daylight/test/test_parser.py +118 -0
  113. chython/files/daylight/test/test_tokenize.py +77 -0
  114. chython/files/daylight/tokenize.py +376 -0
  115. chython/files/libinchi/__init__.py +22 -0
  116. chython/files/libinchi/libinchi.dll +0 -0
  117. chython/files/libinchi/wrapper.py +554 -0
  118. chython/files/mdl/__init__.py +25 -0
  119. chython/files/mdl/emol.py +251 -0
  120. chython/files/mdl/erxn.py +67 -0
  121. chython/files/mdl/mol.py +186 -0
  122. chython/files/mdl/read.py +223 -0
  123. chython/files/mdl/rxn.py +67 -0
  124. chython/files/mdl/stereo.py +63 -0
  125. chython/files/mdl/write.py +201 -0
  126. chython/files/opsin.py +35 -0
  127. chython/files/test/__init__.py +0 -0
  128. chython/files/test/test_inchi.py +27 -0
  129. chython/files/test/test_mrv.py +47 -0
  130. chython/files/test/test_sdf_rdf.py +64 -0
  131. chython/files/test/test_xyz.py +20 -0
  132. chython/files/xyz.py +78 -0
  133. chython/periodictable/__init__.py +69 -0
  134. chython/periodictable/base/__init__.py +25 -0
  135. chython/periodictable/base/dynamic.py +149 -0
  136. chython/periodictable/base/element.py +451 -0
  137. chython/periodictable/base/groups.py +90 -0
  138. chython/periodictable/base/periods.py +46 -0
  139. chython/periodictable/base/query.py +473 -0
  140. chython/periodictable/base/vector.py +119 -0
  141. chython/periodictable/groupI.py +252 -0
  142. chython/periodictable/groupII.py +223 -0
  143. chython/periodictable/groupIII.py +1154 -0
  144. chython/periodictable/groupIV.py +210 -0
  145. chython/periodictable/groupIX.py +196 -0
  146. chython/periodictable/groupV.py +192 -0
  147. chython/periodictable/groupVI.py +183 -0
  148. chython/periodictable/groupVII.py +162 -0
  149. chython/periodictable/groupVIII.py +161 -0
  150. chython/periodictable/groupX.py +166 -0
  151. chython/periodictable/groupXI.py +164 -0
  152. chython/periodictable/groupXII.py +158 -0
  153. chython/periodictable/groupXIII.py +239 -0
  154. chython/periodictable/groupXIV.py +262 -0
  155. chython/periodictable/groupXV.py +276 -0
  156. chython/periodictable/groupXVI.py +457 -0
  157. chython/periodictable/groupXVII.py +324 -0
  158. chython/periodictable/groupXVIII.py +260 -0
  159. chython/periodictable/test/__init__.py +0 -0
  160. chython/periodictable/test/test_element.py +100 -0
  161. chython/reactor/__init__.py +24 -0
  162. chython/reactor/base.py +222 -0
  163. chython/reactor/reactor.py +177 -0
  164. chython/reactor/test/__init__.py +18 -0
  165. chython/reactor/test/test_reactor.py +40 -0
  166. chython/reactor/test/test_transformer.py +78 -0
  167. chython/reactor/transformer.py +72 -0
  168. chython/utils/__init__.py +50 -0
  169. chython/utils/free_wilson.py +114 -0
  170. chython/utils/grid.py +130 -0
  171. chython/utils/retro.py +139 -0
  172. chython-2.19.dist-info/METADATA +105 -0
  173. chython-2.19.dist-info/RECORD +175 -0
  174. chython-2.19.dist-info/WHEEL +4 -0
  175. chython-2.19.dist-info/licenses/LICENSE +165 -0
chython/files/PDBrw.py ADDED
@@ -0,0 +1,255 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2020-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ from itertools import islice
20
+ from pathlib import Path
21
+ from typing import Optional, Sequence, Iterator, List
22
+ from .xyz import xyz
23
+ from ..containers import MoleculeContainer
24
+ from ..exceptions import BufferOverflow
25
+
26
+
27
+ one_symbol_names = {'ALA', 'CYS', 'ASP', 'GLU', 'PHE', 'GLY', 'HIS', 'ILE', 'LYS', 'LEU', 'MET', 'ASN', 'PRO', 'GLN',
28
+ 'ARG', 'SER', 'THR', 'VAL', 'TRP', 'TYR', # Amino acids
29
+ 'ASH', # H-asparagine
30
+ 'HID', 'HIE', 'HIP', # H-histidines
31
+ 'DA', 'DC', 'DG', 'DT', 'DI', # Deoxyribonucleotides
32
+ 'A', 'C', 'G', 'U', 'I', # Ribonucleotides
33
+ 'IOD', 'K'} # Elements
34
+ two_symbol_names = {'CL', 'BR', 'NA', 'CA', 'MG', 'CO', 'MN', 'FE', 'CU', 'ZN'}
35
+
36
+
37
+ class PDBRead:
38
+ """PDB 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
+ Supported multiple structures in same file separated by ENDMDL. Supported only ATOM and HETATM parsing.
43
+ END or ENDMDL required in the end.
44
+ """
45
+ molecule_cls = MoleculeContainer
46
+
47
+ def __init__(self, file, *, buffer_size=10000, ignore: bool = True, element_name_priority: bool = False,
48
+ parse_as_single: bool = False, atom_name_map=None, charge_map: Optional[Sequence[int]] = None,
49
+ radical_map: Optional[Sequence[int]] = None, radius_multiplier: float = 1.25):
50
+ """
51
+ :param ignore: Skip some checks of data or try to fix some errors.
52
+ :param element_name_priority: For ligands use element symbol column value and ignore atom name column.
53
+ :param parse_as_single: Usable if all models in file is the same structure.
54
+ 2d graph will be restored only from first model.
55
+ :param atom_name_map: dictionary with atom names replacements. e.g.: {'Ow': 'O'}. Keys should be capitalized.
56
+ :param charge_map: iterable with total charges of each model in file.
57
+ :param radical_map: iterable with total radicals count of each model in file.
58
+ """
59
+ if isinstance(file, str):
60
+ self.__file = open(file)
61
+ self.__is_buffer = False
62
+ elif isinstance(file, Path):
63
+ self.__file = file.open()
64
+ self.__is_buffer = False
65
+ elif hasattr(file, '__iter__') and hasattr(file, 'read'):
66
+ self.__file = file
67
+ self.__is_buffer = True
68
+ else:
69
+ raise TypeError('invalid file. file-like object or path to file expected')
70
+
71
+ self.__radius_multiplier = radius_multiplier
72
+ self.__ignore = ignore
73
+ self.__element_name_priority = element_name_priority
74
+ self.__parse_as_single = parse_as_single
75
+ self.__parsed_first = None
76
+ self.__atom_name_map = atom_name_map or {}
77
+ self.__charge_map = charge_map
78
+ self.__radical_map = radical_map
79
+
80
+ self.__buffer = None
81
+ self.__buffer_size = buffer_size
82
+ self.__tell = 0
83
+
84
+ def read(self, amount: Optional[int] = None) -> List[MoleculeContainer]:
85
+ """
86
+ Parse whole file
87
+
88
+ :param amount: number of records to read
89
+ """
90
+ if amount:
91
+ return list(islice(iter(self), amount))
92
+ return list(iter(self))
93
+
94
+ def read_structure(self, *, current: bool = True) -> MoleculeContainer:
95
+ """
96
+ Read Molecule container.
97
+
98
+ :param current: return current structure if already parsed, otherwise read next
99
+ """
100
+ data = self._read_block(current=current)
101
+ element_name_priority = self.__element_name_priority
102
+ atom_name_map = self.__atom_name_map
103
+
104
+ atoms = []
105
+ res = []
106
+ charges = []
107
+ log = []
108
+ for line in data:
109
+ if line.startswith(('ATOM', 'HETATM')):
110
+ # COLUMNS DATA TYPE FIELD DEFINITION
111
+ # -------------------------------------------------------------------------------------
112
+ # 1 - 6 Record name "ATOM " or "HETATM"
113
+ # 7 - 11 Integer serial Atom serial number.
114
+ # 13 - 16 Atom name Atom name.
115
+ # 17 Character altLoc Alternate location indicator.
116
+ # 18 - 20 Residue name resName Residue name.
117
+ # 22 Character chainID Chain identifier.
118
+ # 23 - 26 Integer resSeq Residue sequence number.
119
+ # 27 AChar iCode Code for insertion of residues.
120
+ # 31 - 38 Real(8.3) x Orthogonal coordinates for X in Angstroms.
121
+ # 39 - 46 Real(8.3) y Orthogonal coordinates for Y in Angstroms.
122
+ # 47 - 54 Real(8.3) z Orthogonal coordinates for Z in Angstroms.
123
+ # 55 - 60 Real(6.2) occupancy Occupancy.
124
+ # 61 - 66 Real(6.2) tempFactor Temperature factor.
125
+ # 77 - 78 LString(2) element Element symbol, right-justified.
126
+ # 79 - 80 LString(2) charge Charge on the atom.
127
+ charge = line[78:80].strip()
128
+ if charge:
129
+ charge = int(charge)
130
+ else:
131
+ charge = None
132
+ x, y, z = float(line[30:38]), float(line[38:46]), float(line[46:54])
133
+ element = line[76:78].strip()
134
+ residue = line[17:20].strip()
135
+ atom_name = line[12:16].strip(' 0123456789-+')
136
+ if residue in one_symbol_names: # bio-polymers and I
137
+ atom_name = atom_name[0]
138
+ elif residue in two_symbol_names:
139
+ atom_name = atom_name[:2].capitalize()
140
+ elif residue == 'MSE':
141
+ if atom_name.startswith('SE'):
142
+ atom_name = 'Se'
143
+ else:
144
+ atom_name = atom_name[0]
145
+ elif residue == 'CBR':
146
+ if atom_name.startswith('BR'):
147
+ atom_name = 'Br'
148
+ else:
149
+ atom_name = atom_name[0]
150
+ # ligands
151
+ elif element_name_priority:
152
+ atom_name = element
153
+ else:
154
+ atom_name = atom_name.capitalize()
155
+ atom_name = atom_name_map.get(atom_name, atom_name)
156
+
157
+ if atom_name != element:
158
+ log.append(f'Atom name and Element symbol is not equal: {line[:-1]}')
159
+ if not self.__ignore:
160
+ raise ValueError('Atom name and Element symbol is not equal')
161
+ atoms.append((atom_name, x, y, z))
162
+ res.append(residue)
163
+ charges.append(charge)
164
+
165
+ if not atoms:
166
+ raise ValueError('invalid PDB')
167
+
168
+ if self.__charge_map:
169
+ t_charge = self.__charge_map[self.__tell - 1]
170
+ radical = self.__radical_map[self.__tell - 1]
171
+ else:
172
+ t_charge = radical = 0
173
+
174
+ if self.__parsed_first is None:
175
+ mol = xyz(atoms, charge=t_charge, radical=radical, radius_multiplier=self.__radius_multiplier,
176
+ atom_charge=charges, _cls=self.molecule_cls)
177
+
178
+ mol.meta['RESIDUE'] = dict(enumerate(res, 1))
179
+ if log:
180
+ mol.meta['chython_parsing_log'] = log
181
+ if self.__parse_as_single:
182
+ self.__parsed_first = mol.copy()
183
+ return mol
184
+ else:
185
+ if len(self.__parsed_first) != len(atoms):
186
+ raise ValueError('models not equal')
187
+ c = {}
188
+ for (n, a), (e, x, y, z) in zip(self.__parsed_first.atoms(), atoms):
189
+ if a.atomic_symbol != e:
190
+ raise ValueError('models or atom order not equal')
191
+ c[n] = (x, y, z)
192
+ mol = self.__parsed_first.copy()
193
+ mol._conformers[0] = c
194
+ if log:
195
+ if 'chython_parsing_log' in mol.meta:
196
+ mol.meta['chython_parsing_log'] = mol.meta['chython_parsing_log'] + log
197
+ else:
198
+ mol.meta['chython_parsing_log'] = log
199
+ return mol
200
+
201
+ def close(self, force: bool = False):
202
+ """
203
+ Close opened file
204
+
205
+ :param force: force closing of externally opened file or buffer
206
+ """
207
+ if not self.__is_buffer or force:
208
+ self.__file.close()
209
+
210
+ def tell(self):
211
+ """
212
+ Number of records processed from the original file
213
+ """
214
+ return self.__tell
215
+
216
+ def __enter__(self):
217
+ return self
218
+
219
+ def __exit__(self, _type, value, traceback):
220
+ self.close()
221
+
222
+ def __iter__(self) -> Iterator[MoleculeContainer]:
223
+ while True:
224
+ try:
225
+ yield self.read_structure(current=False)
226
+ except ValueError:
227
+ pass
228
+ except EOFError:
229
+ return
230
+
231
+ def __next__(self) -> MoleculeContainer:
232
+ return next(iter(self))
233
+
234
+ def _read_block(self, *, current=True) -> List[str]:
235
+ if current and self.__buffer:
236
+ return self.__buffer
237
+ self.__buffer = None
238
+ buffer_size = self.__buffer_size
239
+ buffer = []
240
+
241
+ for n, line in enumerate(self.__file):
242
+ buffer.append(line)
243
+ if line.startswith('END'):
244
+ break
245
+ elif n == buffer_size:
246
+ raise BufferOverflow
247
+ else:
248
+ raise EOFError
249
+
250
+ self.__tell += 1
251
+ self.__buffer = buffer
252
+ return buffer
253
+
254
+
255
+ __all__ = ['PDBRead']
chython/files/RDFrw.py ADDED
@@ -0,0 +1,309 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2014-2026 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(MOLWrite):
200
+ """
201
+ MDL RDF files writer. works similar to opened for writing file object. support `with` context manager.
202
+ on initialization accept opened for writing in text mode file, string path to file,
203
+ pathlib.Path object or another buffered writer object
204
+ """
205
+ def __init__(self, file, *, append: bool = False, mapping: bool = True):
206
+ """
207
+ :param append: append to existing file (True) or rewrite it (False). For buffered writer object append = False
208
+ will write RDF header and append = True will omit the header.
209
+ :param mapping: write atom mapping.
210
+ """
211
+ super().__init__(file, append=append, mapping=mapping)
212
+ if not append or not (self._is_buffer or self._file.tell() != 0):
213
+ self.write = self.__write
214
+
215
+ def __write(self, data):
216
+ del self.write
217
+ self._file.write(strftime('$RDFILE 1\n$DATM %m/%d/%y %H:%M\n'))
218
+ self.write(data)
219
+
220
+ def write(self, data: Union[ReactionContainer, MoleculeContainer]):
221
+ file = self._file
222
+ if isinstance(data, ReactionContainer):
223
+ file.write(f'$RFMT\n$RXN\n{data.name}\n\n\n{len(data.reactants):3d}{len(data.products):3d}')
224
+ if data.reagents:
225
+ file.write(f'{len(data.reagents):3d}\n')
226
+ else:
227
+ file.write('\n')
228
+ for m in chain(data.reactants, data.products, data.reagents):
229
+ file.write('$MOL\n')
230
+ self._write_molecule(m)
231
+ else:
232
+ file.write('$MFMT\n')
233
+ self._write_molecule(data)
234
+ for k, v in data.meta.items():
235
+ file.write(f'$DTYPE {k}\n$DATUM {v}\n')
236
+
237
+
238
+ class ERDFWrite(EMOLWrite):
239
+ """
240
+ MDL V3000 RDF files writer. works similar to opened for writing file object. support `with` context manager.
241
+ on initialization accept opened for writing in text mode file, string path to file,
242
+ pathlib.Path object or another buffered writer object
243
+ """
244
+ def __init__(self, file, *, append: bool = False, mapping: bool = True, absolute: bool = False):
245
+ """
246
+ :param append: append to existing file (True) or rewrite it (False). For buffered writer object append = False
247
+ will write RDF header and append = True will omit the header.
248
+ :param mapping: write atom mapping.
249
+ :param absolute: explicitly write MDLV30/STEABS collection for stereocenters without extended stereo groups.
250
+ """
251
+ super().__init__(file, append=append, mapping=mapping, absolute=absolute)
252
+ if not append or not (self._is_buffer or self._file.tell() != 0):
253
+ self.write = self.__write
254
+
255
+ def __write(self, data):
256
+ del self.write
257
+ self._file.write(strftime('$RDFILE 1\n$DATM %m/%d/%y %H:%M\n'))
258
+ self.write(data)
259
+
260
+ def write(self, data: Union[ReactionContainer, MoleculeContainer]):
261
+ file = self._file
262
+ if isinstance(data, ReactionContainer):
263
+ file.write(f'$RFMT\n$RXN V3000\n{data.name}\n\n\nM V30 COUNTS {len(data.reactants)} {len(data.products)}')
264
+ if data.reagents:
265
+ file.write(f' {len(data.reagents)}\nM V30 BEGIN REACTANT\n')
266
+ else:
267
+ file.write('\nM V30 BEGIN REACTANT\n')
268
+ for m in data.reactants:
269
+ self._write_molecule(m)
270
+ file.write('M V30 END REACTANT\nM V30 BEGIN PRODUCT\n')
271
+ for m in data.products:
272
+ self._write_molecule(m)
273
+ file.write('M V30 END PRODUCT\n')
274
+ if data.reagents:
275
+ file.write('M V30 BEGIN AGENT\n')
276
+ for m in data.reagents:
277
+ self._write_molecule(m)
278
+ file.write('M V30 END AGENT\n')
279
+ file.write('M END\n')
280
+ else:
281
+ file.write(f'$MFMT\n{data.name}\n\n\n 0 0 0 0 0 999 V3000\n')
282
+ self._write_molecule(data)
283
+ file.write('M END\n')
284
+ for k, v in data.meta.items():
285
+ file.write(f'$DTYPE {k}\n$DATUM {v}\n')
286
+
287
+
288
+ def mdl_rxn(data: str, /, *, ignore=True, calc_cis_trans=False, ignore_stereo=False, remap=False,
289
+ ignore_bad_isotopes=False, _r_cls=ReactionContainer, _m_cls=MoleculeContainer) -> ReactionContainer:
290
+ """
291
+ Parse string with rxn file.
292
+ """
293
+ data = data.splitlines()
294
+ if not data[0].startswith('$RXN'):
295
+ raise ValueError('invalid RXN')
296
+ if data[4].startswith('M V30 COUNTS'):
297
+ tmp = parse_rxn_v3000(data, ignore=ignore)
298
+ else:
299
+ tmp = parse_rxn_v2000(data, ignore=ignore)
300
+
301
+ postprocess_parsed_reaction(tmp, remap=remap, ignore=ignore)
302
+ rxn = create_reaction(tmp, ignore_bad_isotopes=ignore_bad_isotopes, _m_cls=_m_cls, _r_cls=_r_cls)
303
+ if not ignore_stereo:
304
+ for mol, tmp in zip(rxn.molecules(), chain(tmp['reactants'], tmp['reagents'], tmp['products'])):
305
+ postprocess_molecule(mol, tmp, calc_cis_trans=calc_cis_trans)
306
+ return rxn
307
+
308
+
309
+ __all__ = ['RDFRead', 'RDFWrite', 'ERDFWrite', 'mdl_rxn']