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,128 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2021-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 typing import TYPE_CHECKING, List, Tuple, Union
20
+ from ._salts import acids, rules
21
+ from ...periodictable import GroupI, GroupII
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from chython import MoleculeContainer
26
+
27
+
28
+ # atomic number constants
29
+ H = 1
30
+ N = 7
31
+
32
+
33
+ class Salts:
34
+ __slots__ = ()
35
+
36
+ def remove_metals(self: 'MoleculeContainer', *, logging=False) -> Union[bool, List]:
37
+ """
38
+ Remove disconnected S-metals and ammonia.
39
+
40
+ :param logging: return deleted atoms list.
41
+ """
42
+ atoms = self._atoms
43
+ bonds = self._bonds
44
+
45
+ metals = []
46
+ for n, a in atoms.items():
47
+ if not bonds[n] and (a == N or isinstance(a, (GroupI, GroupII)) and a != H):
48
+ metals.append(n)
49
+
50
+ if 0 < len(metals) < len(self):
51
+ for n in metals:
52
+ del atoms[n]
53
+ del bonds[n]
54
+
55
+ self.flush_cache(keep_sssr=True)
56
+ if logging:
57
+ return metals
58
+ return True
59
+ elif logging:
60
+ return []
61
+ return False
62
+
63
+ def remove_acids(self: 'MoleculeContainer', *, logging=False) -> Union[bool, List[int]]:
64
+ """
65
+ Remove common acids from organic bases salts.
66
+ Works only for neutral pairs like HA+B. Use `neutralize` before.
67
+
68
+ :param logging: return deleted atoms list.
69
+ """
70
+ if self.connected_components_count > 1:
71
+ log = []
72
+ for c in self.connected_components:
73
+ if self.substructure(c, recalculate_hydrogens=False) in acids:
74
+ log.extend(c)
75
+ if 0 < len(log) < len(self): # prevent singularity
76
+ atoms = self._atoms
77
+ bonds = self._bonds
78
+
79
+ for n in log:
80
+ del atoms[n]
81
+ del bonds[n]
82
+
83
+ self.flush_cache()
84
+ if logging:
85
+ return log
86
+ return True
87
+ if logging:
88
+ return []
89
+ return False
90
+
91
+ def split_metal_salts(self: 'MoleculeContainer', *, logging=False) -> Union[bool, List[Tuple[int, int]]]:
92
+ """
93
+ Split connected S-metal salts to cation/anion pairs.
94
+
95
+ :param logging: return deleted bonds list.
96
+ """
97
+ atoms = self._atoms
98
+ bonds = self._bonds
99
+
100
+ metals = [n for n, a in atoms.items() if isinstance(a, (GroupI, GroupII)) and a != H]
101
+ if metals:
102
+ acceptors = set()
103
+ log = []
104
+ for q in rules:
105
+ for mapping in q.get_mapping(self, automorphism_filter=False):
106
+ acceptors.add(mapping[1])
107
+
108
+ for n in metals:
109
+ for m in acceptors & bonds[n].keys():
110
+ if atoms[n].charge == 4: # prevent overcharging
111
+ break
112
+ del bonds[n][m]
113
+ del bonds[m][n]
114
+ atoms[n]._charge += 1
115
+ atoms[m]._charge -= 1
116
+ log.append((n, m))
117
+ if log:
118
+ self.flush_cache()
119
+ self.fix_stereo()
120
+ if logging:
121
+ return log
122
+ return True
123
+ if logging:
124
+ return []
125
+ return False
126
+
127
+
128
+ __all__ = ['Salts']
@@ -0,0 +1,378 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2021-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 collections import defaultdict
20
+ from itertools import product
21
+ from operator import itemgetter
22
+ from random import shuffle
23
+ from typing import TYPE_CHECKING, Dict, Optional, Union, List
24
+ from ...containers.bonds import Bond
25
+ from ...exceptions import ValenceError
26
+
27
+
28
+ if TYPE_CHECKING:
29
+ from chython import MoleculeContainer
30
+
31
+ # atom, charge, unsaturation
32
+ tuned_priority = {(7, 0, 0): -3, # amine
33
+ (7, 0, 1): -3, # X=N-X
34
+ (7, 0, 2): -3, # X#N
35
+ (7, 1, 1): -2, # X=[N+](-X)-X
36
+ (7, 1, 2): -2, # X=[N+]=X
37
+ (7, -1, 0): -1, # X-[N-]-X
38
+ (7, -1, 1): -1, # X=[N-]
39
+ (8, 0, 0): -2, # X-O-X
40
+ (8, 0, 1): -2, # X=O
41
+ (8, -1, 0): -1, # X-[O-]
42
+ (8, 1, 1): -1, # X=[O+]-X
43
+ (16, 0, 0): -3, # X-S-X
44
+ (16, 0, 2): -2, # X-S(-X)(=X)=X
45
+ (16, 0, 1): -2, # X-S(-X)(=X)
46
+ (16, 0, 2): -2, # X=S=X
47
+ (16, 0, 0): -1, # X-S(-X)(-X)-X
48
+ (16, 0, 1): -1, # X-[S+](-X)(-X)
49
+ (16, 1, 1): -1, # X=[S+]-X
50
+ }
51
+ charge_priority = {0: 0, -1: 1, 1: 2, 2: 3, 3: 4, -2: 5, 4: 6, -3: 7, -4: 8}
52
+
53
+
54
+ class Saturation:
55
+ __slots__ = ()
56
+
57
+ def saturate(self: 'MoleculeContainer', neighbors_distances: Optional[Dict[int, Dict[int, float]]] = None,
58
+ reset_electrons: bool = True, expected_charge: int = 0, expected_radicals_count: int = 0,
59
+ allow_errors: bool = True, logging: bool = False) -> Union[bool, List[str]]:
60
+ """
61
+ Saturate molecules with double and triple bonds and charges and radical states to correct valences of atoms.
62
+ Note: works only with fully explicit hydrogens!
63
+
64
+ :param neighbors_distances: If given longest bonds can be removed if need.
65
+ :param reset_electrons: Can change charges and radicals if need.
66
+ :param expected_charge: Reset charge to given. Works only with reset_electrons=True.
67
+ :param expected_radicals_count: Reset radical atoms count to given. Works only with reset_electrons=True.
68
+ :param allow_errors: allow unbalanced result.
69
+ :param logging: return log.
70
+ """
71
+ if any(b != 1 for _, _, b in self.bonds()):
72
+ raise ValenceError('only single bonded skeleton can be saturated')
73
+ atoms = self._atoms
74
+ if not reset_electrons:
75
+ expected_radicals_count = sum(a.is_radical for a in atoms.values())
76
+ expected_charge = int(self)
77
+
78
+ if reset_electrons:
79
+ charges = {x: None for x in self}
80
+ radicals = {x: None for x in self}
81
+ else:
82
+ charges = {n: a.charge for n, a in self.atoms()}
83
+ radicals = {n: a.is_radical for n, a in self.atoms()}
84
+ sat, adjacency = _find_possible_valences(atoms, neighbors_distances or self._bonds,
85
+ charges, radicals, neighbors_distances is not None)
86
+ charges = {} # new charge states
87
+ radicals = {} # new radical states
88
+ bonds = {n: {} for n in atoms} # new bonds
89
+
90
+ seen = set()
91
+ unsaturated = {}
92
+ for n, env in adjacency.items(): # set single bonds in molecule. collect unsaturated atoms
93
+ s = sat[n]
94
+ if len(s) == 1:
95
+ c, r, h = s.pop()
96
+ if not h:
97
+ seen.add(n)
98
+ charges[n] = c
99
+ radicals[n] = r
100
+ for m in env:
101
+ if m not in seen:
102
+ bonds[n][m] = bonds[m][n] = Bond(1)
103
+ else:
104
+ unsaturated[n] = [(c, r, h)]
105
+ else:
106
+ # radicals have the lowest priority
107
+ # tuned priority
108
+ # multiple bonds have higher priority
109
+ # charges priority: 0>-1>1>2>3>-2>4>-3>-4
110
+ unsaturated[n] = sorted(s, key=lambda x: (x[1],
111
+ tuned_priority.get((atoms[n].atomic_number, x[0], x[2]), 0),
112
+ -x[2], charge_priority[x[0]]))
113
+
114
+ log = []
115
+ if (need_radicals := expected_radicals_count - sum(radicals.values())) < 0:
116
+ log.append('Radical state not balanced')
117
+ if not allow_errors:
118
+ if logging:
119
+ return log
120
+ return False
121
+ need_radicals = 0 # reset to zero
122
+
123
+ if unsaturated:
124
+ # create graph of unsaturated atoms
125
+ bonds_graph = {n: {m for m in adjacency[n] if m in unsaturated} for n in unsaturated}
126
+ order = list(unsaturated)
127
+ # try to saturate with different random states
128
+ for _ in range(len(unsaturated)):
129
+ shuffle(order)
130
+ sb, sa, log_ = _saturate({n: bonds_graph[n].copy() for n in order}, unsaturated, need_radicals,
131
+ expected_charge - sum(charges.values()))
132
+ if not log_: # success
133
+ break
134
+ else: # failed
135
+ if log_ == 1:
136
+ log.append('Charge state not balanced')
137
+ elif log_ == 2:
138
+ log.append('Radical state not balanced')
139
+ else:
140
+ log.append('Charge state not balanced')
141
+ log.append('Radical state not balanced')
142
+ if not allow_errors: # all attempts failed
143
+ if logging:
144
+ return log
145
+ return False
146
+
147
+ for n, m, b in sb:
148
+ bonds[n][m] = bonds[m][n] = Bond(b)
149
+ for n, c, r in sa:
150
+ charges[n] = c
151
+ radicals[n] = r
152
+ elif expected_charge != sum(charges.values()): # check charge for saturated case
153
+ log.append('Charge state not balanced')
154
+ if not allow_errors:
155
+ if logging:
156
+ return log
157
+ return False
158
+ # reset molecule
159
+ self._bonds = bonds
160
+ for n, r in radicals.items():
161
+ atoms[n]._is_radical = r
162
+ for n, c in charges.items():
163
+ atoms[n]._charge = c
164
+ for a in atoms.values():
165
+ a._implicit_hydrogens = 0 # reset invalid hydrogens counts.
166
+ self.flush_cache()
167
+ self.calc_labels()
168
+ if logging:
169
+ if not log: # check for errors
170
+ log.append('Saturated successfully')
171
+ else:
172
+ log.append('Saturated with errors')
173
+ return log
174
+ return True
175
+
176
+
177
+ def _find_possible_valences(atoms, neighbors_distances, charges, radicals, allow_deleting=True):
178
+ if allow_deleting:
179
+ possible_bonds = {n: md.copy() for n, md in neighbors_distances.items()}
180
+ else:
181
+ possible_bonds = {n: list(md) for n, md in neighbors_distances.items()}
182
+ while True:
183
+ saturation = defaultdict(set)
184
+ for n, env in possible_bonds.items():
185
+ env_atoms = None
186
+ el = len(env)
187
+ dc = charges[n]
188
+ dr = radicals[n]
189
+ for charge, is_radical, valence, implicit, explicit_dict in atoms[n]._compiled_saturation_rules:
190
+ if valence < el or dc is not None and dc != charge or dr is not None and dr != is_radical:
191
+ continue # skip impossible rules
192
+ if explicit_dict:
193
+ if env_atoms is None: # lazy caching
194
+ env_atoms = defaultdict(int)
195
+ for m in env:
196
+ env_atoms[atoms[m].atomic_number] += 1
197
+ env_atoms_copy = env_atoms.copy()
198
+ for (b, a), c in explicit_dict.items(): # stage 1. find explicit valence
199
+ if env_atoms_copy[a] < c: # `c` always > 0
200
+ break # rule not matched
201
+ env_atoms_copy[a] -= c
202
+ else: # stage 2. find possible valence
203
+ if unmatched := sum(env_atoms_copy.values()): # number of atoms outside rule
204
+ if implicit >= unmatched:
205
+ # number of implicit H should be greater or equal to number of neighbors
206
+ saturation[n].add((charge, is_radical, valence - el))
207
+ else: # pattern fully matched. difference bw valence and connectivity is unsaturation.
208
+ saturation[n].add((charge, is_radical, valence - el))
209
+ else: # unspecific rule. found possible valence
210
+ saturation[n].add((charge, is_radical, valence - el))
211
+ if n not in saturation: # valence not found
212
+ break
213
+ else: # all atoms passed
214
+ break
215
+ if allow_deleting:
216
+ out = max(env.items(), key=itemgetter(1))[0]
217
+ del possible_bonds[out][n]
218
+ del possible_bonds[n][out]
219
+ else:
220
+ raise ValenceError('Structure has invalid atoms neighbors count and electron states')
221
+ return saturation, possible_bonds
222
+
223
+
224
+ def _saturate(bonds, atoms, expected_radicals_count, expected_charge):
225
+ atoms = {k: v.copy() for k, v in atoms.items()}
226
+ dots = []
227
+ saturation = []
228
+ electrons = []
229
+ while True:
230
+ # get isolated atoms. atoms should be charged or radical
231
+ to_del = []
232
+ for n, env in bonds.items():
233
+ if not env:
234
+ es = [(n, c, r) for c, r, h in atoms[n] if not h]
235
+ if not es:
236
+ raise ValenceError('Saturation impossible. '
237
+ f"Isolated atom ({n}) doesn't have appropriate charge-radical state")
238
+ to_del.append(n)
239
+ dots.append(es)
240
+ for n in to_del:
241
+ del bonds[n]
242
+ if not bonds:
243
+ break
244
+
245
+ try: # get terminal atom
246
+ n = next(n for n, ms in bonds.items() if len(ms) == 1)
247
+ except StopIteration:
248
+ # get ring or linker atom
249
+ n, _ = min(bonds.items(), key=lambda x: len(x[1]))
250
+ m = bonds[n].pop()
251
+ bonds[m].discard(n)
252
+
253
+ for (nc, nr, nh), (i, (mc, mr, mh)) in product(atoms[n], enumerate(atoms[m])):
254
+ if nh == mh:
255
+ saturation.append((n, m, nh + 1))
256
+ electrons.append((n, nc, nr))
257
+ electrons.append((m, mc, mr))
258
+
259
+ for x in bonds.pop(n):
260
+ saturation.append((n, x, 1))
261
+ bonds[x].discard(n)
262
+ for x in bonds.pop(m):
263
+ saturation.append((m, x, 1))
264
+ bonds[x].discard(m)
265
+ break
266
+ elif nh < mh:
267
+ electrons.append((n, nc, nr))
268
+ saturation.append((n, m, nh + 1))
269
+ atoms[m].pop(i)
270
+ atoms[m].insert(i, (mc, mr, mh - nh))
271
+
272
+ for x in bonds.pop(n):
273
+ saturation.append((n, x, 1))
274
+ bonds[x].discard(n)
275
+ break
276
+ elif nh > mh:
277
+ electrons.append((m, mc, mr))
278
+ saturation.append((n, m, mh + 1))
279
+ atoms[n].pop(i)
280
+ atoms[n].insert(i, (nc, nr, nh - mh))
281
+
282
+ for x in bonds.pop(m):
283
+ saturation.append((m, x, 1))
284
+ bonds[x].discard(m)
285
+ break
286
+ else:
287
+ m = bonds.pop(n).pop()
288
+ bonds[m].discard(n)
289
+
290
+ for (nc, nr, nh), (i, (mc, mr, mh)) in product(atoms[n], enumerate(atoms[m])):
291
+ if nh == mh:
292
+ saturation.append((n, m, nh + 1))
293
+ electrons.append((n, nc, nr))
294
+ electrons.append((m, mc, mr))
295
+ for x in bonds.pop(m):
296
+ saturation.append((m, x, 1))
297
+ bonds[x].discard(m)
298
+ break
299
+ elif nh < mh and bonds[m]:
300
+ electrons.append((n, nc, nr))
301
+ saturation.append((n, m, nh + 1))
302
+ atoms[m].pop(i)
303
+ atoms[m].insert(i, (mc, mr, mh - nh))
304
+ break
305
+ else:
306
+ saturation.append((n, m, 1))
307
+ if not bonds[m]:
308
+ del bonds[m]
309
+
310
+ combo_ua = [] # possible single atoms electron states
311
+ for s in dots:
312
+ if len(s) == 1:
313
+ electrons.extend(s)
314
+ elif s:
315
+ combo_ua.append(s)
316
+
317
+ # if < 0 - we already in bad situation
318
+ # if > 0 - we need more radicals
319
+ need_radical = expected_radicals_count - sum(x for _, _, x in electrons)
320
+ need_charge = expected_charge - sum(x for _, x, _ in electrons)
321
+ if combo_ua:
322
+ # try randomly set charges and radicals.
323
+ # first pick required radical states.
324
+ # second try to minimize charge delta.
325
+ for attempt in range(1, len(combo_ua) + 1):
326
+ shuffle(combo_ua)
327
+ charges_radicals = []
328
+ rad = []
329
+ chg = []
330
+ for atom in combo_ua:
331
+ if len(rad) < need_radical: # pick radicals
332
+ r = next((x for x in atom if x[2]), None)
333
+ if r: # pick random radical states
334
+ rad.append(r)
335
+ else: # not radical
336
+ chg.append(atom)
337
+ else: # pick not radical states
338
+ c = [x for x in atom if not x[2]]
339
+ if len(c) > 1:
340
+ chg.append(c)
341
+ elif c:
342
+ charges_radicals.extend(c)
343
+ elif attempt == len(combo_ua): # all states has radical. balancing impossible
344
+ chg.append(atom) # fuck it horse. we in last attempt
345
+ else: # do next attempt
346
+ break
347
+ else:
348
+ charges_radicals.extend(rad)
349
+ current_charge = need_charge - sum(x for _, x, _ in charges_radicals)
350
+ current_radical = need_radical - len(rad)
351
+ for x in chg:
352
+ n, c, r = min(x, key=lambda x: abs(current_charge - x[1]))
353
+ charges_radicals.append((n, c, r))
354
+ current_charge -= c
355
+ current_radical -= r
356
+
357
+ if current_radical: # radical unbalanced
358
+ if current_charge:
359
+ log = 3
360
+ else:
361
+ log = 2
362
+ elif current_charge:
363
+ log = 1
364
+ else: # balanced!
365
+ log = 0
366
+ break
367
+
368
+ electrons.extend(charges_radicals)
369
+ elif need_radical:
370
+ log = 3 if need_charge else 2
371
+ elif need_charge:
372
+ log = 1
373
+ else:
374
+ log = 0
375
+ return saturation, electrons, log
376
+
377
+
378
+ __all__ = ['Saturation']
@@ -0,0 +1,18 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 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
+ #
@@ -0,0 +1,118 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 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 chython import smiles
20
+ from pytest import mark
21
+
22
+
23
+ data = [
24
+ ('CP(C)(C)C', 'C[P+](C)(C)C'),
25
+ ('CB1(C)[H]B(C)(C)[H]1', 'CB1(C)~[H]B(C)(C)~[H]1'),
26
+ ('[O]N(C)[NH]', '[O-][N+](C)=N'), ('[O]N(C)[CH2]', '[O-][N+](C)=C'),
27
+ ('[O]S(C)(C)[O]', 'O=S(C)(C)=O'), ('[O]S(C)(C)[S]', 'O=S(C)(C)=S'),
28
+ ('BN(C)=C', 'B~N(C)=C'),
29
+ ('B=N(C)(C)C', 'B~N(C)(C)C'),
30
+ ('BS(C)C', 'B~S(C)C'), ('BO(C)C', 'B~O(C)C'),
31
+ ('[B-]=[N+](C)C', 'BN(C)C'), ('C[B-]=[N+]C', 'CBNC'), ('[B-]=[N+]', 'BN'),
32
+ ('[O-][B+3]([O-])([O-])[O-]', 'O[B-](O)(O)O'),
33
+ ('[O-]B(O)(O)O', 'O[B-](O)(O)O'),
34
+ ('OB(O)(O)O', 'O[B-](O)(O)O'),
35
+ ('CN(C)(C)C', 'C[N+](C)(C)C'),
36
+ ('C=N(=O)O', 'C[N+](=O)[O-]'),
37
+ ('C=N(=O)C', 'C=[N+]([O-])C'), ('O=N(=O)C', 'O=[N+]([O-])C'), ('N=N(=O)C', 'N=[N+]([O-])C'),
38
+ ('C=[N+]([O-])O', 'C[N+](=O)[O-]'),
39
+ ('CN(=O)=N(=O)C', 'C[N+]([O-])=[N+]([O-])C'),
40
+ ('CN(=O)=N(=N)C', 'C[N+]([O-])=[N+]([NH-])C'), ('CN(=O)=N(=NC)C', 'C[N+]([O-])=[N+]([N-]C)C'),
41
+ ('C=N(=N)C', 'C=[N+]([N-])C'), ('C=N(=NC)C', 'C=[N+]([N-]C)C'), ('N=N(=N)C', 'N=[N+]([N-])C'),
42
+ ('[N-][N+](=O)C', 'N=[N+]([O-])C'), ('C[N-][N+](=O)C', 'CN=[N+]([O-])C'),
43
+ ('[O-]N(=O)=O', '[O-][N+](=O)[O-]'),
44
+ ('CN(:O):O', 'C[N+](=O)[O-]'),
45
+ ('O=[N-]=O', '[O-]N=O'),
46
+ ('O=N#N', 'O=[N+]=[N-]'), ('C=N#N', 'C=[N+]=[N-]'), ('N=N#N', 'N=[N+]=[N-]'),
47
+ ('[O-][N+]#N', 'O=[N+]=[N-]'), ('C[CH-][N+]#N', 'CC=[N+]=[N-]'), ('[NH-][N+]#N', 'N=[N+]=[N-]'),
48
+ ('C[N+]#N=[N-]', 'CN=[N+]=[N-]'),
49
+ ('CN=N=N', 'CN=[N+]=[N-]'),
50
+ ('CNN#N', 'CN=[N+]=[N-]'),
51
+ ('[N-]#N=NC', '[N-]=[N+]=NC'),
52
+ ('[N-]=N#N', '[N-]=[N+]=[N-]'),
53
+ ('CC#N=N', 'CC=[N+]=[N-]'),
54
+ ('CC#N=NC', 'CC#[N+][N-]C'), ('CC#N=O', 'CC#[N+][O-]'),
55
+ ('NN#C', '[NH-][N+]#C'), ('ON#CC', '[O-][N+]#CC'), ('SN#CC', '[S-][N+]#CC'),
56
+ ('CNN#C', 'C[N-][N+]#C'),
57
+ ('N[N+]#[C-]', '[NH-][N+]#C'), ('O[N+]#[C-]', '[O-][N+]#C'), ('S[N+]#[C-]', '[S-][N+]#C'),
58
+ ('CN[N+]#[C-]', 'C[N-][N+]#C'),
59
+ ('CN#C', 'C[N+]#[C-]'),
60
+ ('C[C-]=[N+]=N', 'CC=[N+]=[N-]'),
61
+ ('CN(C)(C)=O', 'C[N+](C)(C)[O-]'), ('CN(C)(C)=NC', 'C[N+](C)(C)[N-]C'),
62
+ ('C[N](C)=O |^1:1|', 'CN(C)[O] |^1:3|'),
63
+ ('C=N(C)[O] |^1:3|', 'C=[N+](C)[O-]'),
64
+ ('CN(C)#N', 'C[N+](C)=[N-]'),
65
+ ('CN=[N+]', 'C[N+]#N'),
66
+ ('[NH2+][C-]=O', 'N=C=O'), ('C[NH+][C-]=O', 'CN=C=O'),
67
+ ('N#CO', 'N=C=O'),
68
+ ('N#C[O-]', '[N-]=C=O'),
69
+ ('CC(C)(C)[N+][O-]', 'CC(C)(C)N=O'),
70
+ ('CN=O', 'C=NO'),
71
+ ('NC=[O+]C', '[NH2+]=COC'), ('CNC=[O+]C', 'C[NH+]=COC'), ('CN(C)C=[O+]C', 'C[N+](C)=COC'),
72
+ ('N=CO', 'NC=O'), ('N=CS', 'NC=S'),
73
+ ('O=C1NC=CC=C1', 'OC1=NC=CC=C1'), ('OC1=NC=CC=C1', 'OC1=NC=CC=C1'), ('N=C1NC=CC=C1', 'NC1=NC=CC=C1'),
74
+ ('CN=C1NC=CC=C1', 'CNC1=NC=CC=C1'),
75
+ ('O=C1C=CNC=C1', 'OC1=CC=NC=C1'), ('OC1=CC=NC=C1', 'OC1=CC=NC=C1'),
76
+ ('C=C(O)O', 'CC(=O)O'), ('C=C(O)N', 'CC(=O)N'),
77
+ ('OC=C', 'O=CC'), ('OC(C)=C', 'O=C(C)C'),
78
+ ('O=C1N=CC=CC1', 'OC=1N=CC=CC=1'), ('OC=1N=CC=CC=1', 'OC=1N=CC=CC=1'), ('N=C1N=CC=CC1', 'NC=1N=CC=CC=1'),
79
+ ('CN=C1N=CC=CC1', 'CNC=1N=CC=CC=1'),
80
+ ('[O-][P+](C)(C)C', 'O=P(C)(C)C'),
81
+ ('[CH2+][P-](C)(C)C', 'C=P(C)(C)C'),
82
+ ('FP(F)(F)(F)(F)F', 'F[P-](F)(F)(F)(F)F'),
83
+ ('C[P-](C)(=O)=O', 'CP(C)(=O)[O-]'),
84
+ ('CP(C)(=O)[S-]', 'CP(C)(=S)[O-]'),
85
+ ('CP(C)(=O)S', 'CP(C)(=S)O'),
86
+ ('CS(=O)(=O)[S-]', 'CS(=O)(=S)[O-]'),
87
+ ('CS(=O)(=O)S', 'CS(=O)(=S)O'),
88
+ ('C[S+](C)[O-]', 'CS(C)=O'),
89
+ ('O=[S+][O-]', 'O=S=O'), ('O=[S+](C)(C)[O-]', 'O=S(C)(C)=O'),
90
+ ('O=[S+2]([O-])[O-]', 'O=S(=O)=O'),
91
+ ('C[S+2](C)([O-])[O-]', 'CS(C)(=O)=O'),
92
+ ('C[S-](C)[CH2+]', 'CS(C)=C'),
93
+ ('O=[S-](C)=O', 'O=S(C)[O-]'),
94
+ ('O=[S-](C)(=O)=O', 'O=S(C)(=O)[O-]'),
95
+ ('O=[S-](=O)[S-]', 'S=S([O-])[O-]'),
96
+ ('CS(C)(=O)O', 'CS(C)(=O)=O'),
97
+ ('CS(C)(=O)N', 'CS(C)(=O)=N'),
98
+ ('N=S(C)O', 'NS(C)=O'), ('N=S(C)(C)(C)O', 'NS(C)(C)(C)=O'),
99
+ ('C#CO', 'C=C=O'),
100
+ ('C#CNC', 'C=C=NC'),
101
+ ('C=O |^1:0|', '[C-]#[O+]'),
102
+ ('[O]O[O] |^1:0,2|', 'O=[O+][O-]'),
103
+ ('[CH2+]N(C)C', 'C=[N+](C)C'),
104
+ ('[CH2+]N(C)O', 'C=[N+](C)O'),
105
+ ('[CH2+]=NC', 'C#[N+]C'),
106
+ ('O[Cl+][O-]', 'OCl=O'),
107
+ ('O[Cl+2]([O-])[O-]', 'OCl(=O)=O'),
108
+ ('O[Cl+3]([O-])([O-])[O-]', 'OCl(=O)(=O)=O'),
109
+ ('[Cl-]=O', 'Cl[O-]'),
110
+ ('OS(=N)(=N)O', 'O=S(N)(N)=O'), ('OS(=N)(=N)C', 'O=S(N)(=N)C')
111
+ ]
112
+
113
+
114
+ @mark.parametrize('raw,result', data)
115
+ def test_group(raw, result):
116
+ tmp = smiles(raw)
117
+ tmp.standardize()
118
+ assert tmp == smiles(result), f'{raw} > {tmp} != {result}'