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/SDFrw.py ADDED
@@ -0,0 +1,221 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2014-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 collections import defaultdict
20
+ from io import BytesIO
21
+ from pickle import dump
22
+ from re import compile, match
23
+ from subprocess import check_output
24
+ from sys import platform
25
+ from typing import Optional, List
26
+ from .mdl import MDLRead, MOLWrite, EMOLWrite, parse_mol_v2000, parse_mol_v3000, postprocess_molecule
27
+ from ._convert import create_molecule
28
+ from ._mapping import postprocess_parsed_molecule
29
+ from ..containers import MoleculeContainer
30
+ from ..exceptions import BufferOverflow, InvalidMolBlock
31
+
32
+
33
+ meta_pattern = compile(r'^>([^<]+)<([^>]+)>([^><]*)$')
34
+
35
+
36
+ class SDFRead(MDLRead):
37
+ """
38
+ MDL SDF 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
+ escape_map = {'&gt;': '>', '&lt;': '<'}
43
+ molecule_cls = MoleculeContainer
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_end = None
64
+
65
+ def read_structure(self, *, current=True) -> MoleculeContainer:
66
+ data = self._read_mol(current=current)
67
+ if data[4].startswith('M V30 BEGIN CTAB'):
68
+ tmp = parse_mol_v3000(data)
69
+ else:
70
+ tmp = parse_mol_v2000(data)
71
+
72
+ postprocess_parsed_molecule(tmp, remap=self._remap, ignore=self._ignore)
73
+ mol = create_molecule(tmp, ignore_bad_isotopes=self._ignore_bad_isotopes, _cls=self.molecule_cls)
74
+ if not self._ignore_stereo:
75
+ postprocess_molecule(mol, tmp, calc_cis_trans=self._calc_cis_trans)
76
+ meta = self.read_metadata()
77
+ if meta:
78
+ mol.meta.update(meta)
79
+ return mol
80
+
81
+ def read_metadata(self, *, current=True):
82
+ mkey = None
83
+ meta = defaultdict(list)
84
+ for line in self._read_metadata(current=current):
85
+ if matched := match(meta_pattern, line):
86
+ mkey = ' '.join(y for x in matched.groups() if (y := x.strip()))
87
+ for e, s in self.escape_map.items():
88
+ mkey = mkey.replace(e, s)
89
+ elif mkey:
90
+ if line := line.strip():
91
+ meta[mkey].append(line)
92
+ else:
93
+ meta['chython_unparsed_metadata'].append(line.strip())
94
+ return {k: '\n'.join(v) for k, v in meta.items()}
95
+
96
+ def read_mol(self, *, current: bool = True) -> str:
97
+ """
98
+ Read MOL block without metadata
99
+ """
100
+ return ''.join(self._read_mol(current=current))
101
+
102
+ def seek(self, offset):
103
+ super().seek(offset)
104
+ self.__m_end = None
105
+
106
+ def reset_index(self):
107
+ if platform != 'win32' and not self._is_buffer:
108
+ shifts = [0]
109
+ for x in BytesIO(check_output(['grep', '-bE', r'\$\$\$\$', self._file.name])):
110
+ pos, line = x.split(b':', 1)
111
+ shifts.append(int(pos) + len(line))
112
+ shifts.pop(-1)
113
+ with open(self._cache_path, 'wb') as f:
114
+ dump(shifts, f)
115
+ self._shifts = shifts
116
+ else:
117
+ raise NotImplementedError('Indexable supported in unix-like o.s. and for files stored on disk')
118
+
119
+ def _read_block(self, *, current=True) -> List[str]:
120
+ if current and self._buffer:
121
+ return self._buffer
122
+ self.__m_end = m_end = None
123
+ self._buffer = buffer = []
124
+ buffer_size = self._buffer_size
125
+
126
+ for n, line in enumerate(self._file):
127
+ if line.startswith('$$$$'):
128
+ break
129
+ elif n == buffer_size:
130
+ raise BufferOverflow
131
+ buffer.append(line)
132
+ if not m_end and line.startswith('M END'):
133
+ self.__m_end = m_end = len(buffer)
134
+ if buffer:
135
+ self._tell += 1
136
+ else:
137
+ raise EOFError
138
+ return buffer
139
+
140
+ def _read_mol(self, *, current: bool = True) -> List[str]:
141
+ data = self._read_block(current=current)
142
+ if not self.__m_end:
143
+ raise InvalidMolBlock
144
+ return data[:self.__m_end]
145
+
146
+ def _read_metadata(self, *, current: bool = True):
147
+ data = self._read_block(current=current)
148
+ if not self.__m_end:
149
+ raise InvalidMolBlock
150
+ return data[self.__m_end:]
151
+
152
+
153
+ class SDFWrite(MOLWrite):
154
+ """
155
+ MDL SDF files writer. works similar to opened for writing file object. support `with` context manager.
156
+ on initialization accept opened for writing in text mode file, string path to file,
157
+ pathlib.Path object or another buffered writer object
158
+ """
159
+ escape_map = {'>': '&gt;', '<': '&lt;'}
160
+
161
+ def write(self, data: MoleculeContainer, write3d: Optional[int] = None):
162
+ """
163
+ write single molecule into file
164
+
165
+ :param write3d: write conformer coordinates with given index
166
+ """
167
+ self._write_molecule(data, write3d=write3d)
168
+
169
+ file = self._file
170
+ for k, v in data.meta.items():
171
+ for e, s in self.escape_map.items():
172
+ k = k.replace(e, s)
173
+ file.write(f'> <{k}>\n{v}\n\n')
174
+ file.write('$$$$\n')
175
+
176
+
177
+ class ESDFWrite(EMOLWrite):
178
+ """
179
+ MDL V3000 SDF files writer. works similar to opened for writing file object. support `with` context manager.
180
+ on initialization accept opened for writing in text mode file, string path to file,
181
+ pathlib.Path object or another buffered writer object
182
+ """
183
+ escape_map = {'>': '&gt;', '<': '&lt;'}
184
+
185
+ def write(self, data: MoleculeContainer, write3d: Optional[int] = None):
186
+ """
187
+ write single molecule into file
188
+
189
+ :param write3d: write conformer coordinates with given index
190
+ """
191
+ file = self._file
192
+ file.write(f'{data.name}\n\n\n 0 0 0 0 0 999 V3000\n')
193
+ self._write_molecule(data, write3d)
194
+ file.write('M END\n')
195
+
196
+ for k, v in data.meta.items():
197
+ for e, s in self.escape_map.items():
198
+ k = k.replace(e, s)
199
+ file.write(f'> <{k}>\n{v}\n\n')
200
+ file.write('$$$$\n')
201
+
202
+
203
+ def mdl_mol(data: str, /, *, ignore=True, calc_cis_trans=False, ignore_stereo=False, remap=False,
204
+ ignore_bad_isotopes=False, _cls=MoleculeContainer) -> MoleculeContainer:
205
+ """
206
+ Parse string with mol file.
207
+ """
208
+ data = data.splitlines()
209
+ if data[4].startswith('M V30 BEGIN CTAB'):
210
+ tmp = parse_mol_v3000(data)
211
+ else:
212
+ tmp = parse_mol_v2000(data)
213
+
214
+ postprocess_parsed_molecule(tmp, remap=remap, ignore=ignore)
215
+ mol = create_molecule(tmp, ignore_bad_isotopes=ignore_bad_isotopes, _cls=_cls)
216
+ if not ignore_stereo:
217
+ postprocess_molecule(mol, tmp, calc_cis_trans=calc_cis_trans)
218
+ return mol
219
+
220
+
221
+ __all__ = ['SDFRead', 'SDFWrite', 'ESDFWrite', 'mdl_mol']
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2014-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 .daylight import *
20
+ from .libinchi import *
21
+ from .MRVrw import *
22
+ from .opsin import *
23
+ from .PDBrw import *
24
+ from .RDFrw import *
25
+ from .SDFrw import *
26
+ from .xyz import *
27
+
28
+
29
+ __all__ = ['smiles', 'smarts', 'mdl_mol', 'mdl_rxn', 'xyz', 'xyz_file', 'inchi', 'opsin', 'iupac']
30
+ __all__.extend(x for x in locals() if x.endswith(('Read', 'Write')))
@@ -0,0 +1,207 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2023-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 ..containers import MoleculeContainer, ReactionContainer
20
+ from ..containers.bonds import Bond
21
+ from ..exceptions import AtomNotFound
22
+ from ..periodictable import Element
23
+
24
+
25
+ # atomic number constants
26
+ B = 5
27
+ C = 6
28
+ N = 7
29
+ P = 15
30
+
31
+
32
+ def create_molecule(data, *, ignore_bad_isotopes=False, skip_calc_implicit=False,
33
+ keep_implicit=False, keep_radicals=True, ignore_aromatic_radicals=True, ignore=True,
34
+ ignore_carbon_radicals=False, _cls=MoleculeContainer):
35
+ g = _cls()
36
+ g._name = data.get('title')
37
+ atoms = g._atoms
38
+ bonds = g._bonds
39
+ mapping = data['mapping']
40
+
41
+ if any(a.get('z') for a in data['atoms']):
42
+ # store conformer
43
+ g._conformers = [{n: (a['x'], a['y'], a['z']) for n, a in zip(mapping, data['atoms'])}]
44
+
45
+ for n, atom in zip(mapping, data['atoms']):
46
+ e = Element.from_symbol(atom.pop('element'))
47
+ atom.pop('z', None) # clean up MDL
48
+ try:
49
+ atoms[n] = e(**atom)
50
+ except (ValueError, TypeError):
51
+ if not ignore_bad_isotopes:
52
+ raise
53
+ del atom['isotope'] # reset isotope mark on errors and try again.
54
+ atoms[n] = e(**atom)
55
+ bonds[n] = {}
56
+
57
+ for n, m, b in data['bonds']:
58
+ n, m = mapping[n], mapping[m]
59
+ if n == m:
60
+ raise ValueError('atom loops impossible')
61
+ if n not in bonds or m not in bonds:
62
+ raise AtomNotFound('atoms not found')
63
+ if n in bonds[m]:
64
+ raise ValueError('atoms already bonded')
65
+ bonds[n][m] = bonds[m][n] = Bond(b)
66
+
67
+ g.calc_labels() # set all labels except rings
68
+
69
+ if data.get('log'): # store log to the meta
70
+ if data.get('meta') is None:
71
+ data['meta'] = {}
72
+ data['meta']['chython_parsing_log'] = data['log']
73
+ g._meta = data.get('meta') or None
74
+
75
+ if skip_calc_implicit: # don't calc Hs. e.g. INCHI
76
+ return g
77
+
78
+ implicit_mismatch = {}
79
+ radicalized = []
80
+ # precalculate Hs
81
+ for n, a in atoms.items():
82
+ if a.implicit_hydrogens is None:
83
+ # let's try to calculate. in case of errors just keep as is. radicals in smiles should be in [brackets],
84
+ # thus has implicit Hs value
85
+ g.calc_implicit(n)
86
+ elif keep_implicit:
87
+ # keep given Hs count as is
88
+ continue
89
+ else: # recheck given Hs count
90
+ h = a.implicit_hydrogens # parsed Hs
91
+ g.calc_implicit(n) # recalculate
92
+ if a.implicit_hydrogens is None: # atom has invalid valence or aromatic ring.
93
+ if a.hybridization == 4:
94
+ # this is aromatic ring. just restore given H count.
95
+ a._implicit_hydrogens = h
96
+ # rare H0 case
97
+ if (not keep_radicals and not ignore_aromatic_radicals
98
+ and not h and not a.charge and not a.is_radical and a in (B, C, N, P)
99
+ and sum(b != 8 for b in bonds[n].values()) == 2):
100
+ # c[c]c - aromatic B,C,N,P radical
101
+ a._is_radical = True
102
+ radicalized.append(n)
103
+ elif not keep_radicals and not a.is_radical: # CXSMILES radical not set.
104
+ # SMILES doesn't code radicals. so, let's try to guess.
105
+ a._is_radical = True
106
+ if g.check_implicit(n, h): # radical form is valid
107
+ radicalized.append(n)
108
+ a._implicit_hydrogens = h
109
+ elif ignore: # radical state also has errors.
110
+ a._is_radical = False # reset radical state
111
+ implicit_mismatch[n] = h
112
+ if data.get('log') is None:
113
+ data['log'] = []
114
+ data['log'].append(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
115
+ else:
116
+ raise ValueError(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
117
+ elif h != a.implicit_hydrogens: # H count mismatch.
118
+ if a.hybridization == 4:
119
+ if (not keep_radicals
120
+ and not h and not a.charge and not a.is_radical and a in (B, C, N, P)
121
+ and sum(b != 8 for b in bonds[n].values()) == 2):
122
+ # c[c]c - aromatic B,C,N,P radical
123
+ a._implicit_hydrogens = 0
124
+ a._is_radical = True
125
+ radicalized.append(n)
126
+ elif ignore:
127
+ implicit_mismatch[n] = h
128
+ if data.get('log') is None:
129
+ data['log'] = []
130
+ data['log'].append(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
131
+ else:
132
+ raise ValueError(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
133
+ elif g.check_implicit(n, h): # set another possible implicit state. probably Al, P
134
+ a._implicit_hydrogens = h
135
+ elif not keep_radicals and not a.is_radical: # CXSMILES radical is not set. try radical form
136
+ a._is_radical = True
137
+ if g.check_implicit(n, h):
138
+ a._implicit_hydrogens = h
139
+ radicalized.append(n)
140
+ # radical state also has errors.
141
+ elif ignore:
142
+ a._is_radical = False # reset radical state
143
+ implicit_mismatch[n] = h
144
+ if data.get('log') is None:
145
+ data['log'] = []
146
+ data['log'].append(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
147
+ else:
148
+ raise ValueError(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
149
+ elif ignore: # just ignore it
150
+ implicit_mismatch[n] = h
151
+ if data.get('log') is None:
152
+ data['log'] = []
153
+ data['log'].append(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
154
+ else:
155
+ raise ValueError(f'implicit hydrogen count ({h}) mismatch with calculated on atom {n}')
156
+
157
+ if ignore_carbon_radicals:
158
+ for n in radicalized:
159
+ if (a := atoms[n]) == C:
160
+ a._is_radical = False
161
+ a._implicit_hydrogens += 1
162
+ if data.get('log') is None:
163
+ data['log'] = []
164
+ data['log'].append(f'carbon radical {n} replaced with implicit hydrogen')
165
+ elif radicalized:
166
+ g.meta['chython_radicalized_atoms'] = radicalized
167
+ if data.get('log') and 'chython_parsing_log' not in g.meta:
168
+ g.meta['chython_parsing_log'] = data['log']
169
+ if implicit_mismatch:
170
+ g.meta['chython_implicit_mismatch'] = implicit_mismatch
171
+ return g
172
+
173
+
174
+ def create_reaction(data, *, ignore=True, skip_calc_implicit=False, ignore_bad_isotopes=False,
175
+ keep_implicit=False, keep_radicals=True, ignore_aromatic_radicals=True,
176
+ ignore_carbon_radicals=False, _r_cls=ReactionContainer, _m_cls=MoleculeContainer):
177
+ rc, pr, rg = [], [], []
178
+ for ms, pms, gr in ((rc, data['reactants'], 'reactant'),
179
+ (pr, data['products'], 'products'),
180
+ (rg, data['reagents'], 'reagent')):
181
+ tdl = []
182
+ for n, m in enumerate(pms):
183
+ try:
184
+ ms.append(create_molecule(m, skip_calc_implicit=skip_calc_implicit,
185
+ ignore_bad_isotopes=ignore_bad_isotopes, keep_implicit=keep_implicit,
186
+ keep_radicals=keep_radicals,
187
+ ignore_aromatic_radicals=ignore_aromatic_radicals, ignore=ignore,
188
+ ignore_carbon_radicals=ignore_carbon_radicals, _cls=_m_cls))
189
+ except ValueError as e:
190
+ if not ignore:
191
+ raise
192
+ if data.get('log') is None:
193
+ data['log'] = []
194
+ data['log'].append(f'ignored {gr} molecule {n} with {e}')
195
+ tdl.append(n)
196
+ if tdl: # ad-hoc for later postprocessing
197
+ for n in reversed(tdl):
198
+ del pms[n]
199
+
200
+ if data.get('log'): # store log to the meta
201
+ if data.get('meta') is None:
202
+ data['meta'] = {}
203
+ data['meta']['chython_parsing_log'] = data['log']
204
+ return _r_cls(rc, pr, rg, meta=data.get('meta') or None, name=data.get('title'))
205
+
206
+
207
+ __all__ = ['create_molecule']
@@ -0,0 +1,117 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2014-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 count
20
+ from ..exceptions import MappingError
21
+
22
+
23
+ def postprocess_parsed_molecule(data, *, remap=False, ignore=True):
24
+ if remap:
25
+ remapped = list(range(1, len(data['atoms']) + 1))
26
+ else:
27
+ length = count(max(x.get('parsed_mapping') or 0 for x in data['atoms']) + 1)
28
+ remapped, used = [], set()
29
+ for n, atom in enumerate(data['atoms']):
30
+ m = atom.get('parsed_mapping')
31
+ if not m:
32
+ remapped.append(next(length))
33
+ elif m in used:
34
+ if not ignore:
35
+ raise MappingError('mapping in molecules should be unique')
36
+ remapped.append(next(length))
37
+ if data.get('log') is None:
38
+ data['log'] = []
39
+ data['log'].append(f'mapping in molecule changed from {m} to {remapped[n]}')
40
+ else:
41
+ remapped.append(m)
42
+ used.add(m)
43
+ data['mapping'] = remapped
44
+
45
+
46
+ def postprocess_parsed_reaction(data, *, remap=False, ignore=True):
47
+ maps = {'reactants': [], 'products': [], 'reagents': []}
48
+ for i, tmp in maps.items():
49
+ for molecule in data[i]:
50
+ used = set()
51
+ for atom in molecule['atoms']:
52
+ m = atom.get('parsed_mapping')
53
+ if m:
54
+ if m in used:
55
+ if not ignore:
56
+ raise MappingError('mapping in molecules should be unique')
57
+ molecule['log'].append(f'non-unique mapping in molecule: {m}')
58
+ else:
59
+ used.add(m)
60
+ tmp.append(m)
61
+ else:
62
+ tmp.append(0)
63
+
64
+ length = count(max(max(maps['products'], default=0), max(maps['reactants'], default=0),
65
+ max(maps['reagents'], default=0)) + 1)
66
+
67
+ # map unmapped atoms.
68
+ for i, tmp in maps.items():
69
+ used = set()
70
+ maps[i] = _remap = []
71
+ for m in tmp:
72
+ if not m:
73
+ _remap.append(next(length))
74
+ elif m in used:
75
+ if not ignore:
76
+ raise MappingError('mapping in reagents or products or reactants should be unique')
77
+ # force remap non unique atoms in molecules.
78
+ _remap.append(next(length))
79
+ if data.get('log') is None:
80
+ data['log'] = []
81
+ data['log'].append(f'mapping in {i} changed from {m} to {_remap[-1]}')
82
+ else:
83
+ _remap.append(m)
84
+ used.add(m)
85
+
86
+ if maps['reagents']:
87
+ tmp = (set(maps['reactants']) | set(maps['products'])) & set(maps['reagents'])
88
+ if tmp:
89
+ e = f'reagents has map intersection with reactants or products: {tmp}'
90
+ if not ignore:
91
+ raise MappingError(e)
92
+ if data.get('log') is None:
93
+ data['log'] = []
94
+ data['log'].append(e)
95
+ maps['reagents'] = [x if x not in tmp else next(length) for x in maps['reagents']]
96
+
97
+ # find breaks in map. e.g. 1,2,5,6. 3,4 - skipped
98
+ if remap:
99
+ lose = sorted(
100
+ set(range(1, next(length))) - set(maps['reactants']) - set(maps['products']) - set(maps['reagents']),
101
+ reverse=True)
102
+ if lose:
103
+ for i, tmp in maps.items():
104
+ if not tmp:
105
+ continue
106
+ for j in lose:
107
+ maps[i] = tmp = [x if x < j else x - 1 for x in tmp]
108
+
109
+ for i, tmp in maps.items():
110
+ shift = 0
111
+ for j in data[i]:
112
+ atom_len = len(j['atoms'])
113
+ j['mapping'] = tmp[shift: atom_len + shift]
114
+ shift += atom_len
115
+
116
+
117
+ __all__ = ['postprocess_parsed_molecule', 'postprocess_parsed_reaction']
Binary file
chython/files/_xyz.pyx ADDED
@@ -0,0 +1,74 @@
1
+ # cython: freethreading_compatible=True
2
+ #
3
+ # Copyright 2022-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
+ cimport cython
20
+ from cpython.mem cimport PyMem_Malloc, PyMem_Free
21
+ from libc.math cimport sqrt
22
+
23
+
24
+ cdef extern from "Python.h":
25
+ dict _PyDict_NewPresized(Py_ssize_t minused)
26
+
27
+
28
+ @cython.boundscheck(False)
29
+ @cython.wraparound(False)
30
+ def possible_bonds(double[:, ::1] xyz not None, double[::1] radii not None, double multiplier):
31
+ cdef unsigned int size, max_bonds, c = 0, b = 0, n, m, k
32
+ cdef double d, nx, ny, nz, rn, mx, my, mz
33
+ cdef dict py_bonds, tmp
34
+
35
+ size = xyz.shape[0]
36
+ max_bonds = size * 10 # each atom has less then 10 neighbors approximately
37
+
38
+ cdef unsigned int *ns = <unsigned int *> PyMem_Malloc(size * sizeof(unsigned int))
39
+ cdef unsigned int *mi = <unsigned int *> PyMem_Malloc(max_bonds * sizeof(unsigned int))
40
+ cdef double *ds = <double *> PyMem_Malloc(max_bonds * sizeof(double))
41
+
42
+ if not ns or not mi or not ds:
43
+ raise MemoryError()
44
+
45
+ for n in range(size - 1):
46
+ nx, ny, nz = xyz[n, 0], xyz[n, 1], xyz[n, 2]
47
+ rn = radii[n]
48
+ for m in range(n + 1, size):
49
+ mx, my, mz = nx - xyz[m, 0], ny - xyz[m, 1], nz - xyz[m, 2]
50
+ d = sqrt(mx * mx + my * my + mz * mz)
51
+ if d <= (rn + radii[m]) * multiplier:
52
+ mi[c] = m
53
+ ds[c] = d
54
+ c += 1
55
+ ns[n] = c
56
+
57
+ # prepare dict of dicts
58
+ py_bonds = _PyDict_NewPresized(size)
59
+ for n in range(size):
60
+ py_bonds[n + 1] = {}
61
+
62
+ for n in range(size - 1):
63
+ c = ns[n]
64
+ n += 1
65
+ tmp = py_bonds[n]
66
+ for m in range(b, c):
67
+ k = mi[m] + 1
68
+ tmp[k] = py_bonds[k][n] = ds[m]
69
+ b = c
70
+
71
+ PyMem_Free(ns)
72
+ PyMem_Free(mi)
73
+ PyMem_Free(ds)
74
+ return py_bonds
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2018-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # Copyright 2019 Artem Mukanov <nostro32@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 .smarts import *
21
+ from .smiles import *
22
+
23
+
24
+ __all__ = ['smiles', 'smarts']