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
@@ -0,0 +1,103 @@
1
+ # -*- coding: utf-8 -*-
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
+ from collections import deque, defaultdict
20
+ from itertools import product
21
+ from ..aromatics.kekule import _kekule_component
22
+ from ...exceptions import InvalidAromaticRing
23
+
24
+
25
+ # atomic number constants
26
+ B = 5
27
+ C = 6
28
+ N = 7
29
+ P = 15
30
+
31
+
32
+ class HeteroArenes:
33
+ __slots__ = ()
34
+
35
+ def _enumerate_hetero_arene_tautomers(self):
36
+ atoms = self._atoms
37
+ bonds = self._bonds
38
+
39
+ rings = defaultdict(list) # aromatic skeleton
40
+ for n, m_bond in bonds.items():
41
+ for m, bond in m_bond.items():
42
+ if bond == 4:
43
+ rings[n].append(m)
44
+ if not rings:
45
+ return
46
+
47
+ acceptors = set()
48
+ donors = set()
49
+ single_bonded = set()
50
+ for n, ms in rings.items():
51
+ a = atoms[n]
52
+ if len(ms) == 2:
53
+ if a in (B, N, P):
54
+ if not a.charge and not a.is_radical:
55
+ # only neutral B, N, P
56
+ if a.implicit_hydrogens: # pyrrole
57
+ donors.add(n)
58
+ elif len(bonds[n]) == 2: # pyridine
59
+ acceptors.add(n)
60
+ else:
61
+ single_bonded.add(n)
62
+ elif a.charge == -1 and a == C: # ferrocene
63
+ single_bonded.add(n)
64
+ elif len(ms) == 3 and a in (B, N, P) and not a.charge and not a.is_radical:
65
+ single_bonded.add(n)
66
+ if not donors or not acceptors:
67
+ return
68
+
69
+ atoms = set(rings)
70
+ components = []
71
+ while atoms:
72
+ start = atoms.pop()
73
+ component = {start: rings[start]}
74
+ queue = deque([start])
75
+ while queue:
76
+ current = queue.popleft()
77
+ for n in rings[current]:
78
+ if n not in component:
79
+ queue.append(n)
80
+ component[n] = rings[n]
81
+
82
+ atoms.difference_update(component)
83
+ if donors.isdisjoint(component) or acceptors.isdisjoint(component):
84
+ continue
85
+ components.append(component)
86
+
87
+ if not components:
88
+ return
89
+ for component in components:
90
+ for d, a in product(component.keys() & donors, component.keys() & acceptors):
91
+ sb = component.keys() & single_bonded
92
+ sb.add(a) # now pyrrole
93
+ try:
94
+ next(_kekule_component(component, sb, (), 0))
95
+ except InvalidAromaticRing:
96
+ continue
97
+ mol = self.copy(keep_sssr=True, keep_components=True)
98
+ mol._atoms[d]._implicit_hydrogens = 0
99
+ mol._atoms[a]._implicit_hydrogens = 1
100
+ yield mol
101
+
102
+
103
+ __all__ = ['HeteroArenes']
@@ -0,0 +1,143 @@
1
+ # -*- coding: utf-8 -*-
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
+ from collections import defaultdict
20
+ from functools import cached_property
21
+ from itertools import chain, repeat
22
+ from ._keto_enol import *
23
+
24
+
25
+ # atomic number constants
26
+ C = 6
27
+
28
+
29
+ class KetoEnol:
30
+ __slots__ = ()
31
+
32
+ def _enumerate_keto_enol_tautomers(self, partial=False):
33
+ for fix, ket in self.__enumerate_bonds(partial):
34
+ if ket:
35
+ a = fix[-1][1]
36
+ d = fix[0][0]
37
+ else:
38
+ a = fix[0][0]
39
+ d = fix[-1][1]
40
+
41
+ mol = self.copy(keep_sssr=True, keep_components=True)
42
+ m_bonds = mol._bonds
43
+ for n, m, b in fix:
44
+ m_bonds[n][m]._order = b
45
+
46
+ a = mol._atoms[a]
47
+ d = mol._atoms[d]
48
+ a._implicit_hydrogens += 1
49
+ d._implicit_hydrogens -= 1
50
+ a._hybridization -= 1 # -C=X>=C-X or -C=C=X>=C-C=X
51
+ d._hybridization += 1
52
+ yield mol, ket
53
+
54
+ @cached_property
55
+ def _sugar_groups(self):
56
+ ek = []
57
+ for mapping in sugar_group.get_mapping(self, automorphism_filter=False):
58
+ e, k = mapping[1], mapping[2]
59
+ ek.append((e, k))
60
+ return ek
61
+
62
+ def __enumerate_bonds(self, partial):
63
+ atoms = self._atoms
64
+ bonds = self._bonds
65
+ rings = self.atoms_rings_sizes
66
+
67
+ # search neutral oxygen and nitrogen
68
+ donors = defaultdict(set)
69
+ acceptors = defaultdict(set)
70
+ for q in enol_rules:
71
+ for mapping in q.get_mapping(self, automorphism_filter=False):
72
+ donors[mapping[1]].add(mapping[2])
73
+ for q in keto_rules:
74
+ for mapping in q.get_mapping(self, automorphism_filter=False):
75
+ acceptors[mapping[1]].add(mapping[2])
76
+
77
+ for (atom, dirs), hydrogen, anti in chain(zip(donors.items(), repeat(True), repeat(acceptors)),
78
+ zip(acceptors.items(), repeat(False), repeat(donors))):
79
+ path = []
80
+ seen = {atom}
81
+ stack = [(atom, n, 2 if hydrogen else 1, 0) for n in dirs]
82
+ while stack:
83
+ last, current, bond, depth = stack.pop()
84
+
85
+ if partial and path and not len(path) % 2 and \
86
+ (hydrogen or # enol > ketone
87
+ atoms[(x := path[-1][1])].implicit_hydrogens and
88
+ (x not in rings or all(x > 7 for x in rings[x]))): # ketone>
89
+ # return partial hops. ignore allenes in small rings.
90
+ yield path, hydrogen
91
+ if len(path) > depth: # fork found
92
+ if not partial and not len(path) % 2 and (hydrogen or atoms[path[-1][1]].implicit_hydrogens):
93
+ # end of path found. return it and start new one.
94
+ yield path, hydrogen
95
+ seen.difference_update(x for _, x, _ in path[depth:])
96
+ path = path[:depth]
97
+
98
+ path.append((last, current, bond))
99
+
100
+ # adding neighbors
101
+ depth += 1
102
+ seen.add(current)
103
+ if bond == 2:
104
+ next_bond = 1
105
+ else:
106
+ next_bond = 2
107
+
108
+ for n, b in bonds[current].items():
109
+ if n == last:
110
+ continue
111
+ elif n in seen: # aromatic ring destruction. pyridine double bonds shift
112
+ continue
113
+ elif n in anti: # enol-ketone switch
114
+ if current in anti[n]: # keton or enol bond
115
+ if hydrogen:
116
+ cp = path.copy()
117
+ cp.append((current, n, 1)) # double to single in keton end
118
+ yield cp, True
119
+ else:
120
+ cp = path.copy()
121
+ cp.append((current, n, 2)) # single to double in enol end
122
+ yield cp, False
123
+ elif b == bond and (a := atoms[n]) == C: # classic keto-enol route
124
+ if a.hybridization == 2: # grow up
125
+ stack.append((current, n, next_bond, depth))
126
+ elif hydrogen:
127
+ if a.hybridization == 3: # OC=CC=C=C case
128
+ cp = path.copy()
129
+ cp.append((current, n, 1))
130
+ yield cp, True # ketone found
131
+ elif a.hybridization == 1 and a.implicit_hydrogens: # ketone >> enol
132
+ cp = path.copy()
133
+ cp.append((current, n, 2))
134
+ yield cp, False
135
+
136
+ if path and not len(path) % 2 and \
137
+ (hydrogen or # enol > ketone
138
+ atoms[(x := path[-1][1])].implicit_hydrogens and
139
+ (x not in rings or all(x > 7 for x in rings[x]))):
140
+ yield path, hydrogen
141
+
142
+
143
+ __all__ = ['KetoEnol']
@@ -0,0 +1,18 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2021-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
+ #
@@ -0,0 +1,80 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2021-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 chython import smiles
20
+ from itertools import zip_longest
21
+
22
+
23
+ def test_keto_enol_2h_pyrrole():
24
+ """
25
+ 2H‐pyrrole. [N:1]=1[C:2][C:3]=,:[C:4][C:5]=1
26
+ """
27
+ for t, v in zip(['C1C=CC=N1', 'C1N=CC2=C1C=CC=C2'], ['C=1C=CNC=1', 'N1C=C2C=CC=CC2=C1']):
28
+ s = smiles(t)
29
+ t = set(s.enumerate_tautomers())
30
+ v = smiles(v)
31
+ s.thiele()
32
+ v.thiele()
33
+ assert len(t) == 2, ' '.join(str(x) for x in t)
34
+ assert t == {s, v}, f'{", ".join(str(x) for x in t)} != {s}, {v}'
35
+
36
+
37
+ def test_acid_protonated_nitrogen():
38
+ """
39
+ Ammonia, H-imine,guanidine,amidine. [H][N+]=,-,:
40
+ """
41
+ for t, v in zip_longest(['C=[NH2+].[Cl-]', 'C=[NH+]C.[Cl-]', 'C[NH3+].[Cl-]', 'C[NH2+]C.[Cl-]', 'C[NH1+](C)C.[Cl-]',
42
+ '[NH4+].[Cl-]', 'C=[N+](C)C.[Cl-]', 'C[N+](C)(C)C.[Cl-]'],
43
+ ['C=N.Cl', 'C=NC.Cl', 'CN.Cl', 'CNC.Cl', 'CN(C)C.Cl', 'N.Cl']):
44
+ s = smiles(t)
45
+ t = set(s.enumerate_tautomers())
46
+ if v:
47
+ assert len(t) == 2, ' '.join(str(x) for x in t)
48
+ assert t == {s, smiles(v)}
49
+ else:
50
+ assert len(t) == 1
51
+
52
+
53
+ def test_base_nitrogen():
54
+ for t, v in zip_longest(['N1C=CN=N1.Cl', 'NC(N)=N.Cl', 'CN(C)C(=NO)N(C)C.Cl', 'CN(C)C(=NC)N(C)C.Cl',
55
+ 'CN(C)C(=NN)N(C)C.Cl', 'COC(N)=N.Cl', 'CSC(N)=N.Cl', 'COC(OC)=N.Cl', 'COC(C)=N.Cl',
56
+ 'CNN.Cl', 'CN.Cl',
57
+ 'N.Cl', 'N1C=CC=C1.Cl'],
58
+ [('N1N=NC=C1.Cl', 'N1=CC=NN1.Cl', '[NH+]=1NC=CN=1.[Cl-]', 'N1=[NH+]C=CN1.[Cl-]',
59
+ 'N1=CC=[NH+]N1.[Cl-]'),
60
+ ('NC(N)=N.Cl', 'NC(N)=[NH2+].[Cl-]'),
61
+ ('CN(C)C(=NO)N(C)C.Cl', 'CN(C)C(N(C)C)=[NH+]O.[Cl-]'),
62
+ ('CN(C)C(=NC)N(C)C.Cl', 'CN(C)C(N(C)C)=[NH+]C.[Cl-]'),
63
+ ('CN(C)C(=NN)N(C)C.Cl', 'CN(C)C(N(C)C)=[NH+]N.[Cl-]'),
64
+ ('Cl.NC(=N)OC', '[NH2+]=C(N)OC.[Cl-]'), ('Cl.NC(=N)SC', '[NH2+]=C(N)SC.[Cl-]'),
65
+ ('COC(OC)=N.Cl', 'COC(OC)=[NH2+].[Cl-]'),
66
+ ('COC(C)=N.Cl', 'COC(C)=[NH2+].[Cl-]'),
67
+ ('CNN.Cl', 'CN[NH3+].[Cl-]', 'C[NH2+]N.[Cl-]'),
68
+ ('CN.Cl', 'C[NH3+].[Cl-]')]):
69
+ s = smiles(t)
70
+ t = set(s.enumerate_tautomers(zwitter=True))
71
+ if v:
72
+ assert len(t) == len(v), ' '.join(str(x) for x in t)
73
+ vs = set()
74
+ for x in v:
75
+ x = smiles(x)
76
+ x.thiele(fix_tautomers=False)
77
+ vs.add(x)
78
+ assert t == vs, ' '.join(str(x) for x in t) + ' != ' + ' '.join(str(x) for x in vs)
79
+ else:
80
+ assert len(t) == 1, ' '.join(str(x) for x in t)
@@ -0,0 +1,18 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2025, 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
+ #
@@ -0,0 +1,58 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2025, 2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # Copyright 2025 Tagir Akhmetshin <tagirshin@gmail.com>
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 chython import smiles
21
+
22
+
23
+ def test_basic():
24
+ # Test basic atom mapping in simple molecules
25
+ mol1 = smiles('CC(=O)O') # acetic acid
26
+ mol2 = smiles('CC(O)=O') # acetic acid
27
+
28
+ assert mol1 <= mol2
29
+ assert mol2 <= mol1
30
+
31
+ mappings = list(mol1.get_mapping(mol2))
32
+ assert len(mappings) == 1
33
+ assert mappings[0] == {1: 1, 2: 2, 3: 4, 4: 3}
34
+ assert not smiles('CC(O)O') <= mol1
35
+ assert smiles('C[O-]') <= smiles('CC[O-]')
36
+ assert not smiles('C[O-]') <= smiles('CCO')
37
+
38
+
39
+ def test_substructure_mapping():
40
+ # Test mapping of a substructure
41
+ mol = smiles('CCC(=O)OC')
42
+ substructure = smiles('CC(=O)O')
43
+
44
+ assert substructure < mol
45
+ mappings = list(substructure.get_mapping(mol))
46
+ assert len(mappings) == 1
47
+ assert mappings[0] == {1: 2, 2: 3, 3: 4, 4: 5}
48
+
49
+
50
+ def test_multiple_mappings():
51
+ # Test cases where multiple valid mappings exist
52
+ mol = smiles('CC(=O)OC(=O)C')
53
+ pattern = smiles('CC(=O)O') # acetone pattern
54
+
55
+ mappings = list(pattern.get_mapping(mol))
56
+ assert len(mappings) == 2 # should find multiple matches
57
+ assert {1: 1, 2: 2, 3: 3, 4: 4} in mappings
58
+ assert {1: 7, 2: 5, 3: 6, 4: 4} in mappings
@@ -0,0 +1,76 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2025, 2026 Ramil Nugmanov <nougmanoff@protonmail.com>
4
+ # Copyright 2025 Tagir Akhmetshin <tagirshin@gmail.com>
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 chython import smiles
21
+
22
+
23
+ def test_basic_smiles():
24
+ # Test basic SMILES generation
25
+ mol = smiles('CO') # methanol
26
+ assert str(mol) in ('CO', 'OC')
27
+
28
+ mol = smiles('c1ccccc1') # benzene
29
+ assert str(mol) == 'c1ccccc1'
30
+
31
+
32
+ def test_format_options():
33
+ # Test different format options
34
+ mol = smiles('C=1C=CC=CC=1')
35
+
36
+ # Test asymmetric closures
37
+ assert str(mol) == 'C=1C=CC=CC=1'
38
+ assert format(mol, 'a') == 'C=1C=CC=CC1'
39
+
40
+ assert format(mol, '!b') == 'C1CCCCC1'
41
+
42
+ # Test disable stereo
43
+ mol = smiles('C[C@H](O)CC')
44
+ assert '@' in str(mol)
45
+ assert '@' not in format(mol, '!s')
46
+
47
+ mol = smiles('c1ccccc1')
48
+ assert format(mol, 'A') == 'C:1:C:C:C:C:C:1'
49
+ assert format(mol, 'Aa') == 'C:1:C:C:C:C:C1'
50
+ assert format(mol, 'm') == '[cH:1]1[cH:2][cH:3][cH:4][cH:5][cH:6]1'
51
+ assert format(mol, 'h') == '[cH]1[cH][cH][cH][cH][cH]1'
52
+
53
+ assert format(mol, 'Ah') == '[CH]:1:[CH]:[CH]:[CH]:[CH]:[CH]:1'
54
+ assert format(mol, 'Ah!b') == '[CH]1[CH][CH][CH][CH][CH]1'
55
+
56
+ mol = smiles('[K+]')
57
+ assert str(mol) == '[K+]'
58
+ assert format(mol, '!z') == '[K]'
59
+
60
+ mol = smiles('[CH3]')
61
+ assert str(mol) == '[CH3] |^1:0|'
62
+ assert format(mol, '!x') == '[CH3]'
63
+
64
+ mol = smiles('CCO')
65
+ assert len({format(mol, 'r') for _ in range(50)}) == 4
66
+
67
+
68
+ def test_smiles_comparison():
69
+ # Test SMILES comparison functionality
70
+ mol1 = smiles('CCO')
71
+ mol2 = smiles('OCC')
72
+ mol3 = smiles('CCC')
73
+
74
+ assert mol1 == mol2 # same molecules
75
+ assert mol1 != mol3 # different molecules
76
+ assert hash(mol1) == hash(mol2) # same hash for same molecules