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
@@ -0,0 +1,157 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2022, 2023 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # This file is part of chython.
5
+ #
6
+ # chython is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation; either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with this program; if not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ from collections import defaultdict
20
+ from ...exceptions import IncorrectSmiles
21
+
22
+
23
+ def parser(tokens, strong_cycle):
24
+ log = []
25
+ t1 = tokens[0][0]
26
+ if t1 == 2:
27
+ if tokens[1][0] not in (0, 8):
28
+ raise IncorrectSmiles('not atom started')
29
+ elif t1 not in (0, 8):
30
+ raise IncorrectSmiles('not atom started')
31
+
32
+ atoms = []
33
+ bonds = []
34
+ order = defaultdict(list)
35
+ atoms_types = []
36
+ atom_num = 0
37
+ last_num = 0
38
+ stack = []
39
+ cycles = {}
40
+ stereo_atoms = {}
41
+ stereo_bonds = defaultdict(dict)
42
+ previous = None
43
+
44
+ for token_type, token in tokens:
45
+ if token_type == 2: # ((((((
46
+ if previous:
47
+ if previous[0] != 4:
48
+ raise IncorrectSmiles('bond before side chain')
49
+ previous = None
50
+ stack.append(last_num)
51
+ elif token_type == 3: # ))))))
52
+ if previous:
53
+ raise IncorrectSmiles('bond before closure')
54
+ try:
55
+ last_num = stack.pop()
56
+ except IndexError:
57
+ raise IncorrectSmiles('close chain more than open')
58
+ elif token_type in (1, 4, 9, 10, 12): # bonds. only keeping for atoms connecting
59
+ if previous:
60
+ raise IncorrectSmiles('2 bonds in a row')
61
+ elif not atoms:
62
+ raise IncorrectSmiles('started from bond')
63
+ previous = (token_type, token)
64
+ elif token_type == 6: # cycle
65
+ if previous and previous[0] == 4:
66
+ raise IncorrectSmiles('dot-cycle pattern invalid')
67
+ elif token not in cycles:
68
+ cycles[token] = (last_num, previous, len(order[last_num]))
69
+ order[last_num].append(None) # Reserve a table
70
+ previous = None
71
+ else:
72
+ a, ob, ind = cycles[token]
73
+ if ob:
74
+ if not previous:
75
+ bt, b = ob
76
+ if bt == 9: # closure open is \/ bonded
77
+ stereo_bonds[a][last_num] = b
78
+ b = 1
79
+ elif strong_cycle:
80
+ raise IncorrectSmiles('not equal cycle bonds')
81
+ else:
82
+ log.append('ignored difference in cycle bonds')
83
+ else:
84
+ bt, b = previous
85
+ obt, ob = ob
86
+ if bt == 9: # \/ bonds can be unequal
87
+ if obt == 9:
88
+ stereo_bonds[a][last_num] = ob
89
+ elif ob != 1:
90
+ raise IncorrectSmiles('not equal cycle bonds')
91
+ stereo_bonds[last_num][a] = b
92
+ b = 1
93
+ elif obt == 9:
94
+ if b != 1:
95
+ raise IncorrectSmiles('not equal cycle bonds')
96
+ stereo_bonds[a][last_num] = ob
97
+ elif b != ob:
98
+ raise IncorrectSmiles('not equal cycle bonds')
99
+ previous = None
100
+ elif previous:
101
+ bt, b = previous
102
+ if bt == 9: # stereo \/
103
+ stereo_bonds[last_num][a] = b
104
+ b = 1
105
+ elif strong_cycle:
106
+ raise IncorrectSmiles('not equal cycle bonds')
107
+ else:
108
+ log.append('ignored difference in cycle bonds')
109
+ previous = None
110
+ else:
111
+ b = 4 if atoms_types[last_num] == atoms_types[a] == 8 else 1
112
+
113
+ bonds.append((last_num, a, b))
114
+ order[a][ind] = last_num
115
+ order[last_num].append(a)
116
+ del cycles[token]
117
+ else: # atom
118
+ if atoms:
119
+ if not previous:
120
+ bonds.append((atom_num, last_num, 4 if token_type == atoms_types[last_num] == 8 else 1))
121
+ order[last_num].append(atom_num)
122
+ order[atom_num].append(last_num)
123
+ else:
124
+ bt, b = previous
125
+ if bt == 9:
126
+ bonds.append((atom_num, last_num, 4 if token_type == atoms_types[last_num] == 8 else 1))
127
+ order[last_num].append(atom_num)
128
+ order[atom_num].append(last_num)
129
+
130
+ stereo_bonds[last_num][atom_num] = b
131
+ stereo_bonds[atom_num][last_num] = not b
132
+ elif bt in (1, 10, 12):
133
+ bonds.append((atom_num, last_num, b))
134
+ order[last_num].append(atom_num)
135
+ order[atom_num].append(last_num)
136
+ # else bt == 4 - skip dot
137
+ previous = None
138
+
139
+ if token.get('stereo') is not None:
140
+ stereo_atoms[atom_num] = token.pop('stereo')
141
+ atoms.append(token)
142
+ atoms_types.append(token_type)
143
+ last_num = atom_num
144
+ atom_num += 1
145
+
146
+ if stack:
147
+ raise IncorrectSmiles('number of ( does not equal to number of )')
148
+ elif cycles:
149
+ raise IncorrectSmiles('cycle is not finished')
150
+ elif previous:
151
+ raise IncorrectSmiles('bond on the end')
152
+
153
+ return {'atoms': atoms, 'bonds': bonds, 'order': order, 'stereo_atoms': stereo_atoms,
154
+ 'stereo_bonds': stereo_bonds, 'log': log}
155
+
156
+
157
+ __all__ = ['parser']
@@ -0,0 +1,107 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2022-2025 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 functools import partial
20
+ from itertools import count
21
+ from re import compile, findall, search
22
+ from .parser import parser
23
+ from .tokenize import smarts_tokenize
24
+ from ...containers import QueryContainer, QueryBond
25
+ from ...periodictable import ListElement, QueryElement
26
+
27
+
28
+ cx_radicals = compile(r'\^[1-7]:[0-9]+(?:,[0-9]+)*')
29
+
30
+ # AD-HOC for masked atoms in SMARTS
31
+ # not thread safe
32
+ global_free_masked = count(10 ** 9 + 1)
33
+
34
+
35
+ def smarts(data: str):
36
+ """
37
+ Parse SMARTS string.
38
+
39
+ * only D, a, h, r and !R atom primitives supported.
40
+ * bond order list (max 2) and not bond supported.
41
+ * [not]ring bond supported only in combination with explicit bonds, not bonds and bonds orders lists.
42
+ * mapping, charge and isotopes supported.
43
+ * list of elements supported.
44
+ * A - treats as any element. <A> primitive (aliphatic) ignored.
45
+ * M - treats as any metal
46
+ * <&> logic operator unsupported.
47
+ * <;> logic operator is mandatory except (however preferable) for charge, isotope, stereo marks.
48
+ * CXSMARTS radicals supported.
49
+ * masked atom - `chython.Reactor` specific mark for masking reactant atoms from deletion.
50
+
51
+ For example::
52
+
53
+ [C;r5,r6;a]-;!@[C;h1,h2;z2,z4] |^1:1| - aromatic C member of 5 or 6 atoms ring
54
+ connected with non-ring single bond to aromatic or SP2 radical C with 1 or 2 hydrogens.
55
+
56
+ * primitive <xN> - heteroatoms (e.g. x2 - two heteroatoms)
57
+ * primitive <zN> - hybridization (N = 1 - sp3, 2 - sp2, 3 - sp, 4 - aromatic)
58
+ * primitive <M> - masked atom
59
+
60
+ Note: atom numbers greater than 10 ** 9 forbidden for usage and reserved for masked atoms numbering.
61
+ In multiprocess mode has potential bugs in reaction enumeration task then used templates prepared from components
62
+ from different processes. For avoiding, prepare templates on single process and then share it.
63
+ """
64
+ if not isinstance(data, str):
65
+ raise TypeError('Must be a SMARTS string')
66
+ smr, *cx = data.split()
67
+
68
+ parsed = parser(smarts_tokenize(smr), False)
69
+
70
+ if cx and cx[0].startswith('|') and cx[0].endswith('|'):
71
+ for x in findall(cx_radicals, cx[0]):
72
+ for i in x[3:].split(','):
73
+ parsed['atoms'][int(i)]['is_radical'] = True
74
+
75
+ for i, s in parsed['stereo_atoms'].items():
76
+ parsed['atoms'][i]['stereo'] = s
77
+ stereo_bonds = parsed['stereo_bonds']
78
+
79
+ g = QueryContainer(data)
80
+
81
+ mapping = {}
82
+ free = count(max(a.get('parsed_mapping', 0) for a in parsed['atoms']) + 1)
83
+ for i, a in enumerate(parsed['atoms']):
84
+ mapping[i] = n = a.pop('parsed_mapping', 0) or next(global_free_masked if a.get('masked') else free)
85
+ e = a.pop('element')
86
+ if isinstance(e, int):
87
+ e = QueryElement.from_atomic_number(e)
88
+ elif isinstance(e, str):
89
+ e = QueryElement.from_symbol(e)
90
+ else:
91
+ e = partial(ListElement, e)
92
+ g.add_atom(e(**a), n)
93
+
94
+ for n, m, b in parsed['bonds']:
95
+ if n in stereo_bonds and m in stereo_bonds:
96
+ if m not in stereo_bonds[n]: # only simple cis-trans supported, not cumulenes
97
+ _, s1 = stereo_bonds[n].popitem()
98
+ _, s2 = stereo_bonds[m].popitem()
99
+ if isinstance(b, int):
100
+ b = QueryBond(b, stereo=s1 == s2)
101
+ else:
102
+ b.stereo = s1 == s2
103
+ g.add_bond(mapping[n], mapping[m], b)
104
+ return g
105
+
106
+
107
+ __all__ = ['smarts']
@@ -0,0 +1,245 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2022-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 itertools import chain
20
+ from re import compile, findall, search
21
+ from typing import List, Union, Optional
22
+ from .parser import parser
23
+ from .tokenize import smiles_tokenize
24
+ from .._convert import create_molecule, create_reaction
25
+ from .._mapping import postprocess_parsed_molecule, postprocess_parsed_reaction
26
+ from ...containers import MoleculeContainer, ReactionContainer
27
+ from ...exceptions import IsChiral, NotChiral, ValenceError
28
+
29
+
30
+ cx_fragments = compile(r'f:(?:[0-9]+(?:\.[0-9]+)+)(?:,(?:[0-9]+(?:\.[0-9]+)+))*')
31
+ cx_radicals = compile(r'\^[1-7]:[0-9]+(?:,[0-9]+)*')
32
+
33
+
34
+ def smiles(data, /, *, ignore: bool = True, remap: bool = False, ignore_stereo: bool = False,
35
+ ignore_bad_isotopes: bool = False, keep_implicit: bool = False, ignore_carbon_radicals: bool = False,
36
+ ignore_aromatic_radicals: bool = True,
37
+ _r_cls=ReactionContainer, _m_cls=MoleculeContainer) -> Union[MoleculeContainer, ReactionContainer]:
38
+ """
39
+ SMILES string parser
40
+
41
+ :param ignore: Skip some checks of data or try to fix some errors.
42
+ :param remap: Remap atom numbers started from one.
43
+ :param ignore_stereo: Ignore stereo data.
44
+ :param keep_implicit: keep given in smiles implicit hydrogen count, otherwise ignore on valence error.
45
+ :param ignore_bad_isotopes: reset invalid isotope mark to non-isotopic.
46
+ :param ignore_carbon_radicals: fill carbon radicals with hydrogen (X[C](X)X case).
47
+ :param ignore_aromatic_radicals: don't treat aromatic tokens like c[c]c as radicals.
48
+ """
49
+ if not isinstance(data, str):
50
+ raise TypeError('Must be a SMILES string')
51
+ elif not data:
52
+ raise ValueError('Empty string')
53
+
54
+ contract: Optional[List[List[int]]] # typing
55
+ log = []
56
+ smi, *data = data.split()
57
+ if data and (cxs := data[0]).startswith('|') and cxs.endswith('|'):
58
+ fr = search(cx_fragments, cxs)
59
+ if fr is not None:
60
+ contract = [sorted(int(x) for x in x.split('.')) for x in fr.group()[2:].split(',')]
61
+ if len({x for x in contract for x in x}) < len([x for x in contract for x in x]):
62
+ log.append(f'collisions in cxsmiles fragments description: {cxs}')
63
+ contract = None
64
+
65
+ radicals = [int(x) for x in findall(cx_radicals, cxs) for x in x[3:].split(',')]
66
+ if radicals and len(set(radicals)) != len(radicals):
67
+ log.append(f'collisions in cxsmiles radicals description: {cxs}')
68
+ radicals = []
69
+ else:
70
+ radicals = [int(x) for x in findall(cx_radicals, cxs) for x in x[3:].split(',')]
71
+ if radicals:
72
+ if len(set(radicals)) != len(radicals):
73
+ log.append(f'collisions in cxsmiles radicals description: {cxs}')
74
+ radicals = []
75
+ contract = None
76
+ else:
77
+ radicals = []
78
+ contract = None
79
+
80
+ if '>' in smi:
81
+ record = {'reactants': [], 'reagents': [], 'products': [], 'log': log}
82
+ try:
83
+ reactants, reagents, products = smi.split('>')
84
+ except ValueError as e:
85
+ raise ValueError('invalid reaction smiles') from e
86
+
87
+ mol_count = 0
88
+ for k, d in zip(('reactants', 'products', 'reagents'), (reactants, products, reagents)):
89
+ if not d:
90
+ continue
91
+ for x in d.split('.'):
92
+ if not x:
93
+ if ignore:
94
+ log.append('two dots in line ignored')
95
+ else:
96
+ raise ValueError('invalid reaction smiles. two dots in line')
97
+ else:
98
+ record[k].append(x)
99
+ mol_count += 1
100
+
101
+ if contract:
102
+ if max(x for x in contract for x in x) >= mol_count:
103
+ log.append(f'skipped invalid contract data: {contract}')
104
+ lr = len(record['reactants'])
105
+ lp = len(record['products'])
106
+ reactants = set(range(lr))
107
+ reagents = set(range(lr, mol_count - lp))
108
+ products = set(range(mol_count - lp, mol_count))
109
+ new_molecules: List[Optional[str]] = [None] * mol_count
110
+ for c in contract:
111
+ if reactants.issuperset(c):
112
+ new_molecules[c[0]] = '.'.join(record['reactants'][x] for x in c)
113
+ reactants.difference_update(c)
114
+ elif products.issuperset(c):
115
+ new_molecules[c[0]] = '.'.join(record['products'][x - mol_count] for x in c)
116
+ products.difference_update(c)
117
+ elif reagents.issuperset(c):
118
+ new_molecules[c[0]] = '.'.join(record['reagents'][x - lr] for x in c)
119
+ reagents.difference_update(c)
120
+ else:
121
+ log.append(f'impossible to contract different parts of reaction: {contract}')
122
+ for x in reactants:
123
+ new_molecules[x] = record['reactants'][x]
124
+ for x in products:
125
+ new_molecules[x] = record['products'][x - mol_count]
126
+ for x in reagents:
127
+ new_molecules[x] = record['reagents'][x - lr]
128
+
129
+ record['reactants'] = [x for x in new_molecules[:lr] if x is not None]
130
+ record['products'] = [x for x in new_molecules[-lp:] if x is not None]
131
+ record['reagents'] = [x for x in new_molecules[lr: -lp] if x is not None]
132
+
133
+ for k in ('reactants', 'products', 'reagents'):
134
+ tmp = []
135
+ for x in record[k]:
136
+ tmp.append(parser(smiles_tokenize(x), not ignore))
137
+ record[k] = tmp
138
+
139
+ if radicals:
140
+ atom_map = dict(enumerate(a for m in chain(record['reactants'], record['reagents'], record['products'])
141
+ for a in m['atoms']))
142
+ for x in radicals:
143
+ atom_map[x]['is_radical'] = True
144
+
145
+ postprocess_parsed_reaction(record, remap=remap, ignore=ignore)
146
+ rxn = create_reaction(record, ignore_bad_isotopes=ignore_bad_isotopes, keep_radicals=False,
147
+ ignore_carbon_radicals=ignore_carbon_radicals, keep_implicit=keep_implicit,
148
+ ignore_aromatic_radicals=ignore_aromatic_radicals, ignore=ignore,
149
+ _r_cls=_r_cls, _m_cls=_m_cls)
150
+ for mol, tmp in zip(rxn.molecules(), chain(record['reactants'], record['reagents'], record['products'])):
151
+ postprocess_molecule(mol, tmp, ignore_stereo=ignore_stereo)
152
+ return rxn
153
+ else:
154
+ record = parser(smiles_tokenize(smi), not ignore)
155
+ for x in radicals:
156
+ record['atoms'][x]['is_radical'] = True
157
+ record['log'].extend(log)
158
+
159
+ postprocess_parsed_molecule(record, remap=remap, ignore=ignore)
160
+ mol = create_molecule(record, ignore_bad_isotopes=ignore_bad_isotopes, keep_radicals=False,
161
+ ignore_carbon_radicals=ignore_carbon_radicals, keep_implicit=keep_implicit,
162
+ ignore_aromatic_radicals=ignore_aromatic_radicals, ignore=ignore,
163
+ _cls=_m_cls)
164
+ postprocess_molecule(mol, record, ignore_stereo=ignore_stereo)
165
+ return mol
166
+
167
+
168
+ def postprocess_molecule(molecule, data, *, ignore_stereo=False):
169
+ mapping = data['mapping']
170
+
171
+ if ignore_stereo:
172
+ return
173
+ elif not data['stereo_atoms'] and not data['stereo_bonds']:
174
+ return
175
+
176
+ atoms = molecule._atoms
177
+ st = molecule.stereogenic_tetrahedrons
178
+ sa = molecule.stereogenic_allenes
179
+ sat = molecule._stereo_allenes_terminals
180
+ ctc = molecule._stereo_cis_trans_counterpart
181
+
182
+ order = {mapping[n]: [mapping[m] for m in ms] for n, ms in data['order'].items()}
183
+
184
+ log = []
185
+ stereo = []
186
+ for i, s in data['stereo_atoms'].items():
187
+ n = mapping[i]
188
+ if not i and atoms[n].implicit_hydrogens: # first atom in smiles has reversed chiral mark
189
+ s = not s
190
+
191
+ if n in st:
192
+ stereo.append((molecule.add_atom_stereo, n, order[n], s))
193
+ elif n in sa:
194
+ t1, t2 = sat[n]
195
+ env = sa[n]
196
+ n1 = next(x for x in order[t1] if x in env)
197
+ n2 = next(x for x in order[t2] if x in env)
198
+ stereo.append((molecule.add_atom_stereo, n, (n1, n2), s))
199
+ else:
200
+ log.append(f'non chiral atom {n} has stereo label in smiles')
201
+
202
+ stereo_bonds = {mapping[n]: {mapping[m]: s for m, s in ms.items()}
203
+ for n, ms in data['stereo_bonds'].items()}
204
+ seen = set()
205
+ for n, ns in stereo_bonds.items():
206
+ if n in seen:
207
+ continue
208
+ if n in ctc:
209
+ m = ctc[n]
210
+ if m in stereo_bonds:
211
+ seen.add(m)
212
+ n2, s2 = stereo_bonds[m].popitem()
213
+ n1, s1 = ns.popitem()
214
+ stereo.append((molecule.add_cis_trans_stereo, n, m, n1, n2, s1 == s2))
215
+
216
+ while stereo:
217
+ fail_stereo = []
218
+ old_stereo = len(stereo)
219
+ for f, *args in stereo:
220
+ try:
221
+ f(*args, clean_cache=False)
222
+ except NotChiral:
223
+ fail_stereo.append((f, *args))
224
+ except IsChiral:
225
+ pass
226
+ except ValenceError:
227
+ log.append('structure has errors, stereo data skipped')
228
+ molecule.flush_cache()
229
+ break
230
+ else:
231
+ stereo = fail_stereo
232
+ if len(stereo) == old_stereo:
233
+ break
234
+ molecule.flush_stereo_cache()
235
+ continue
236
+ break
237
+
238
+ if log:
239
+ if 'chython_parsing_log' not in molecule.meta:
240
+ molecule.meta['chython_parsing_log'] = log
241
+ else:
242
+ molecule.meta['chython_parsing_log'].extend(log)
243
+
244
+
245
+ __all__ = ['smiles']
@@ -0,0 +1,18 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2025 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
+ #