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,88 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 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 lazy_object_proxy import Proxy
20
+
21
+
22
+ def _make_reactor(rxn_name, fg_name, output_fg, product_smarts):
23
+ from ._functional import rules
24
+ from ...reactor import Reactor
25
+ from ... import smarts
26
+
27
+ q = rules[fg_name]
28
+ product = smarts(product_smarts)
29
+ return rxn_name, fg_name, output_fg, Reactor((q,), (product,),
30
+ delete_atoms=True, one_shot=True, ignore_pyrrole_hydrogen=True)
31
+
32
+
33
+ def _rules():
34
+ rules = []
35
+
36
+ # aldehyde → primary_alcohol (NaBH4, LiAlH4)
37
+ rules.append(_make_reactor('aldehyde_to_alcohol', 'aldehyde', 'primary_alcohol', '[A:2]-[A:1]'))
38
+
39
+ # ketone → secondary_alcohol (NaBH4, LiAlH4)
40
+ rules.append(_make_reactor('ketone_to_alcohol', 'ketone', 'secondary_alcohol', '[A:2]-[A:1]'))
41
+
42
+ # carboxylic_acid → primary_alcohol (LiAlH4)
43
+ rules.append(_make_reactor('acid_to_alcohol', 'carboxylic_acid', 'primary_alcohol', '[A:2]-[A:1]'))
44
+
45
+ # ester → alcohol (LiAlH4). OR' group is lost.
46
+ rules.append(_make_reactor('ester_to_alcohol', 'ester', 'primary_alcohol', '[A:2]-[A:1]'))
47
+
48
+ # primary_amide → amine (LiAlH4, BH3)
49
+ rules.append(_make_reactor('amide_to_amine', 'primary_amide', 'primary_amine', '[A:1]-[A:2]'))
50
+
51
+ # secondary_amide → amine (LiAlH4, BH3)
52
+ rules.append(_make_reactor('amide_to_amine', 'secondary_amide', 'secondary_amine', '[A:1]-[A:2]'))
53
+
54
+ # nitrile → primary_amine (LiAlH4, H2/cat)
55
+ rules.append(_make_reactor('nitrile_to_amine', 'nitrile', 'primary_amine', '[A:2]-[A:1]'))
56
+
57
+ # nitro → primary_amine (H2/Pd, SnCl2, Fe/HCl)
58
+ rules.append(_make_reactor('nitro_to_amine', 'nitro', 'primary_aniline', '[A:1]'))
59
+
60
+ # azide → primary_amine (PPh3/H2O, H2/Pd)
61
+ rules.append(_make_reactor('azide_to_amine', 'azide', 'primary_amine', '[A:1]'))
62
+
63
+ # alkene hydrogenation: C=C → C-C (H2/Pd, H2/Pt)
64
+ rules.append(_make_reactor('alkene_hydrogenation', 'alkene', None, '[A:1]-[A:2]'))
65
+ rules.append(_make_reactor('alkene_hydrogenation', 'terminal_alkene', None, '[A:1]-[A:2]'))
66
+ rules.append(_make_reactor('alkene_hydrogenation', 'enamine', None, '[A:1]-[A:2]-[A:3]'))
67
+ rules.append(_make_reactor('alkene_hydrogenation', 'enol_ether', None, '[A:1]-[A:2]-[A:3]'))
68
+
69
+ # deoxygenation: alcohol → alkane (Barton-McCombie, Appel+reduction)
70
+ rules.append(_make_reactor('deoxygenation', 'primary_alcohol', None, '[A:2]'))
71
+ rules.append(_make_reactor('deoxygenation', 'secondary_alcohol', None, '[A:2]'))
72
+
73
+ # carbonyl → primary amine (reductive amination with NH3)
74
+ rules.append(_make_reactor('carbonyl_to_amine', 'aldehyde', 'primary_amine', '[A:1]-[N:20]'))
75
+ rules.append(_make_reactor('carbonyl_to_amine', 'ketone', 'primary_amine', '[A:1]-[N:20]'))
76
+
77
+ # reductive dehalogenation: Ar-X → Ar-H (H2/Pd, n-Bu3SnH, Zn/AcOH)
78
+ rules.append(_make_reactor('debromination', 'aryl_bromide', None, '[A:1]'))
79
+ rules.append(_make_reactor('dechlorination', 'aryl_chloride', None, '[A:1]'))
80
+ rules.append(_make_reactor('deiodination', 'aryl_iodide', None, '[A:1]'))
81
+
82
+ return rules
83
+
84
+
85
+ rules = Proxy(_rules)
86
+
87
+
88
+ __all__ = ['rules']
@@ -0,0 +1,278 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 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 lazy_object_proxy import Proxy
20
+
21
+
22
+ def _make_reactor(rxn_name, fg_name, output_fg, product_smarts):
23
+ from ._functional import rules
24
+ from ...reactor import Reactor
25
+ from ... import smarts
26
+
27
+ q = rules[fg_name]
28
+ product = smarts(product_smarts)
29
+ return rxn_name, fg_name, output_fg, Reactor((q,), (product,),
30
+ delete_atoms=True, one_shot=True, ignore_pyrrole_hydrogen=True)
31
+
32
+
33
+ def _rules():
34
+ rules = []
35
+
36
+ # 1,3-diketone → isoxazole (+ hydroxylamine, implicit)
37
+ rules.append(_make_reactor('isoxazole', '1_3_diketone', 'isoxazole', '[A:1]:1:[O:20]:[N:21]:[A:3]:[A:5]:1'))
38
+
39
+ # 1,4-diketone → pyridazine (+ hydrazine, implicit)
40
+ rules.append(_make_reactor('pyridazine', '1_4_diketone', 'pyridazine', '[A:1]:1:[N:20]:[N:21]:[A:3]:[A:6]:[A:5]:1'))
41
+
42
+ # Appel: alcohol → alkyl halide (CCl4/CBr4 + PPh3)
43
+ rules.append(_make_reactor('appel', 'primary_alcohol', 'alkyl_bromide', '[Br:20]-[A:2]'))
44
+ rules.append(_make_reactor('appel', 'secondary_alcohol', 'alkyl_bromide', '[Br:20]-[A:2]'))
45
+ rules.append(_make_reactor('appel_chloride', 'primary_alcohol', 'alkyl_chloride', '[Cl:20]-[A:2]'))
46
+ rules.append(_make_reactor('appel_chloride', 'secondary_alcohol', 'alkyl_chloride', '[Cl:20]-[A:2]'))
47
+
48
+ # Miyaura borylation: ArX → ArB(OH)2
49
+ rules.append(_make_reactor('borylation_acid', 'aryl_bromide', 'aryl_boronic_acid', '[A:1]-[B:20](-[O:21])-[O:22]'))
50
+ rules.append(_make_reactor('borylation_acid', 'aryl_iodide', 'aryl_boronic_acid', '[A:1]-[B:20](-[O:21])-[O:22]'))
51
+ rules.append(_make_reactor('borylation_acid', 'aryl_chloride', 'aryl_boronic_acid', '[A:1]-[B:20](-[O:21])-[O:22]'))
52
+
53
+ # Miyaura borylation: ArX → ArBpin (Pd, B2pin2)
54
+ rules.append(_make_reactor('borylation_ester', 'aryl_bromide', 'aryl_boronic_ester',
55
+ '[A:1]-[B:20]1-[O:21]-[C:22]([C:23])([C:24])-[C:25]([C:26])([C:27])-[O:28]-1'))
56
+ rules.append(_make_reactor('borylation_ester', 'aryl_iodide', 'aryl_boronic_ester',
57
+ '[A:1]-[B:20]1-[O:21]-[C:22]([C:23])([C:24])-[C:25]([C:26])([C:27])-[O:28]-1'))
58
+ rules.append(_make_reactor('borylation_ester', 'aryl_chloride', 'aryl_boronic_ester',
59
+ '[A:1]-[B:20]1-[O:21]-[C:22]([C:23])([C:24])-[C:25]([C:26])([C:27])-[O:28]-1'))
60
+ rules.append(_make_reactor('borylation_acid', 'aryl_triflate', 'aryl_boronic_acid', '[A:1]-[B:20](-[O:21])-[O:22]'))
61
+ rules.append(_make_reactor('borylation_ester', 'aryl_triflate', 'aryl_boronic_ester',
62
+ '[A:1]-[B:20]1-[O:21]-[C:22]([C:23])([C:24])-[C:25]([C:26])([C:27])-[O:28]-1'))
63
+
64
+ # Nitrile hydrolysis: R-C≡N → R-C(=O)-NH2 (partial, to amide)
65
+ rules.append(_make_reactor('nitrile_hydrolysis', 'nitrile', 'primary_amide', '[A:2]-[A:1]=[O:20]'))
66
+
67
+ # Electrophilic aromatic nitration: Ar-H → Ar-NO2
68
+ rules.append(_make_reactor('nitration', 'arene_ch', 'nitro', '[A:1]-[N;+:20](=[O:21])[O;-:22]'))
69
+
70
+ # Electrophilic aromatic halogenation: Ar-H → Ar-X
71
+ rules.append(_make_reactor('bromination', 'arene_ch', 'aryl_bromide', '[A:1]-[Br:20]'))
72
+ rules.append(_make_reactor('chlorination', 'arene_ch', 'aryl_chloride', '[A:1]-[Cl:20]'))
73
+ rules.append(_make_reactor('iodination', 'arene_ch', 'aryl_iodide', '[A:1]-[I:20]'))
74
+ rules.append(_make_reactor('fluorination', 'arene_ch', 'aryl_fluoride', '[A:1]-[F:20]'))
75
+
76
+ # benzylic halogenation: Ar-CH2R → Ar-CHBrR (NBS, Br2/hv)
77
+ rules.append(_make_reactor('benzylic_bromination', 'benzylic_ch', 'alkyl_bromide', '[A:1](-[Br:20])-[A:2]'))
78
+
79
+ # alpha-halogenation: alpha_ketone → alpha-bromoketone (NBS/Br2)
80
+ rules.append(_make_reactor('alpha_halogenation', 'alpha_ketone', 'alpha_haloketone', '[A:1](=[A:2])-[A:3](-[Br:20])'))
81
+
82
+ # alkyl borylation: R-X → R-B(OH)2
83
+ rules.append(_make_reactor('borylation_acid', 'alkyl_bromide', 'alkyl_boronic_acid', '[A:1]-[B:20](-[O:21])-[O:22]'))
84
+ rules.append(_make_reactor('borylation_acid', 'alkyl_iodide', 'alkyl_boronic_acid', '[A:1]-[B:20](-[O:21])-[O:22]'))
85
+ rules.append(_make_reactor('borylation_acid', 'alkyl_chloride', 'alkyl_boronic_acid', '[A:1]-[B:20](-[O:21])-[O:22]'))
86
+
87
+ # alkyl borylation: R-X → R-Bpin
88
+ rules.append(_make_reactor('borylation_ester', 'alkyl_bromide', 'alkyl_boronic_ester',
89
+ '[A:1]-[B:20]1-[O:21]-[C:22]([C:23])([C:24])-[C:25]([C:26])([C:27])-[O:28]-1'))
90
+ rules.append(_make_reactor('borylation_ester', 'alkyl_iodide', 'alkyl_boronic_ester',
91
+ '[A:1]-[B:20]1-[O:21]-[C:22]([C:23])([C:24])-[C:25]([C:26])([C:27])-[O:28]-1'))
92
+ rules.append(_make_reactor('borylation_ester', 'alkyl_chloride', 'alkyl_boronic_ester',
93
+ '[A:1]-[B:20]1-[O:21]-[C:22]([C:23])([C:24])-[C:25]([C:26])([C:27])-[O:28]-1'))
94
+
95
+ # ester to amide: R-C(=O)-OR' → R-C(=O)-NH2 (NH3)
96
+ rules.append(_make_reactor('ester_to_amide', 'ester', 'primary_amide', '[A:1](=[A:2])-[N:20]'))
97
+
98
+ # ester to hydroxamic acid: R-C(=O)-OR' → R-C(=O)-NH-OH (NH2OH)
99
+ rules.append(_make_reactor('ester_to_hydroxamic_acid', 'ester', 'hydroxamic_acid', '[A:1](=[A:2])-[N:20]-[O:21]'))
100
+
101
+ # amide hydrolysis: R-C(=O)-NHR → R-C(=O)-OH
102
+ rules.append(_make_reactor('amide_hydrolysis', 'primary_amide', 'carboxylic_acid', '[A:2](=[A:3])-[O:20]'))
103
+ rules.append(_make_reactor('amide_hydrolysis', 'secondary_amide', 'carboxylic_acid', '[A:2](=[A:3])-[O:20]'))
104
+
105
+ # cyanation: R-X → R-CN (NaCN/CuCN)
106
+ rules.append(_make_reactor('cyanation', 'alkyl_bromide', 'nitrile', '[A:1]-[C:20]#[N:21]'))
107
+ rules.append(_make_reactor('cyanation', 'alkyl_iodide', 'nitrile', '[A:1]-[C:20]#[N:21]'))
108
+ rules.append(_make_reactor('cyanation', 'alkyl_chloride', 'nitrile', '[A:1]-[C:20]#[N:21]'))
109
+ rules.append(_make_reactor('cyanation', 'aryl_bromide', 'nitrile', '[A:1]-[C:20]#[N:21]'))
110
+ rules.append(_make_reactor('cyanation', 'aryl_iodide', 'nitrile', '[A:1]-[C:20]#[N:21]'))
111
+ rules.append(_make_reactor('cyanation', 'aryl_chloride', 'nitrile', '[A:1]-[C:20]#[N:21]'))
112
+
113
+ # dehydration: R-C(=O)-NH2 → R-C≡N (P2O5/SOCl2)
114
+ rules.append(_make_reactor('dehydration', 'primary_amide', 'nitrile', '[A:2]#[A:1]'))
115
+
116
+ # triflation: phenol/alcohol → triflate (Tf2O)
117
+ rules.append(_make_reactor('triflation', 'phenol', 'aryl_triflate', '[A:2]-[A:1]-[S:20](=[O:21])(=[O:22])-[C:23]([F:24])([F:25])[F:26]'))
118
+ rules.append(_make_reactor('triflation', 'primary_alcohol', 'alkyl_triflate', '[A:2]-[A:1]-[S:20](=[O:21])(=[O:22])-[C:23]([F:24])([F:25])[F:26]'))
119
+ rules.append(_make_reactor('triflation', 'secondary_alcohol', 'alkyl_triflate', '[A:2]-[A:1]-[S:20](=[O:21])(=[O:22])-[C:23]([F:24])([F:25])[F:26]'))
120
+
121
+ # nitrile full hydrolysis: R-C≡N → R-COOH
122
+ rules.append(_make_reactor('nitrile_to_acid', 'nitrile', 'carboxylic_acid', '[A:1](=[O:20])-[O:21]'))
123
+
124
+ # tertiary alcohol dehydration: R3C-OH → R2C=CR (loss of water)
125
+ rules.append(_make_reactor('alcohol_dehydration', 'tertiary_alcohol_with_alpha_h', 'alkene', '[A:2]=[A:3]'))
126
+
127
+ # oximation: ketone/aldehyde → oxime (+ NH2OH implicit)
128
+ rules.append(_make_reactor('oximation', 'ketone', 'oxime', '[O:20]-[N:21]=[A:1]'))
129
+ rules.append(_make_reactor('oximation', 'aldehyde', 'oxime', '[O:20]-[N:21]=[A:1]'))
130
+
131
+ # acid chloride formation: RCOOH → RCOCl (SOCl2, oxalyl chloride)
132
+ rules.append(_make_reactor('acid_chlorination', 'carboxylic_acid', 'acyl_chloride', '[Cl:20]-[A:1]=[A:2]'))
133
+
134
+ # mesylation: ROH → ROMs (MsCl, Et3N)
135
+ rules.append(_make_reactor('mesylation', 'primary_alcohol', 'alkyl_mesylate', '[A:2]-[A:1]-[S:20](=[O:21])(=[O:22])-[C:23]'))
136
+ rules.append(_make_reactor('mesylation', 'secondary_alcohol', 'alkyl_mesylate', '[A:2]-[A:1]-[S:20](=[O:21])(=[O:22])-[C:23]'))
137
+
138
+ # tosylation: ROH → ROTs (TsCl, pyridine)
139
+ rules.append(_make_reactor('tosylation', 'primary_alcohol', 'alkyl_tosylate',
140
+ '[A:2]-[A:1]-[S:20](=[O:21])(=[O:22])-[C:23]:1:[C:24]:[C:25]:[C:26](-[C:27]):[C:28]:[C:29]:1'))
141
+ rules.append(_make_reactor('tosylation', 'secondary_alcohol', 'alkyl_tosylate',
142
+ '[A:2]-[A:1]-[S:20](=[O:21])(=[O:22])-[C:23]:1:[C:24]:[C:25]:[C:26](-[C:27]):[C:28]:[C:29]:1'))
143
+
144
+ # halide hydrolysis: ArX → ArOH (NaOH/H2O, Cu cat, high T)
145
+ rules.append(_make_reactor('halide_hydrolysis', 'aryl_chloride', 'phenol', '[A:1]-[O:20]'))
146
+ rules.append(_make_reactor('halide_hydrolysis', 'aryl_bromide', 'phenol', '[A:1]-[O:20]'))
147
+ rules.append(_make_reactor('halide_hydrolysis', 'aryl_iodide', 'phenol', '[A:1]-[O:20]'))
148
+ rules.append(_make_reactor('halide_hydrolysis', 'aryl_fluoride', 'phenol', '[A:1]-[O:20]'))
149
+
150
+ # alkyl halide hydrolysis: RX → ROH (SN2, NaOH/H2O)
151
+ rules.append(_make_reactor('halide_hydrolysis', 'alkyl_chloride', 'primary_alcohol', '[A:1]-[O:20]'))
152
+ rules.append(_make_reactor('halide_hydrolysis', 'alkyl_bromide', 'primary_alcohol', '[A:1]-[O:20]'))
153
+ rules.append(_make_reactor('halide_hydrolysis', 'alkyl_iodide', 'primary_alcohol', '[A:1]-[O:20]'))
154
+
155
+ # DAST fluorination: ROH → RF (DAST, DeoxoFluor)
156
+ rules.append(_make_reactor('fluorination', 'primary_alcohol', 'alkyl_fluoride', '[F:20]-[A:2]'))
157
+ rules.append(_make_reactor('fluorination', 'secondary_alcohol', 'alkyl_fluoride', '[F:20]-[A:2]'))
158
+ rules.append(_make_reactor('fluorination', 'tertiary_alcohol', 'alkyl_fluoride', '[F:20]-[A:2]'))
159
+
160
+ # Appel iodination: ROH → RI (I2/PPh3)
161
+ rules.append(_make_reactor('appel_iodide', 'primary_alcohol', 'alkyl_iodide', '[I:20]-[A:2]'))
162
+ rules.append(_make_reactor('appel_iodide', 'secondary_alcohol', 'alkyl_iodide', '[I:20]-[A:2]'))
163
+
164
+ # Hydroxy to chloro: ROH → RCl (SOCl2, PCl5 — broader than Appel)
165
+ # phenol → ArCl (PCl5, POCl3)
166
+ rules.append(_make_reactor('hydroxy_to_chloro', 'phenol', 'aryl_chloride', '[Cl:20]-[A:2]'))
167
+ # tertiary_alcohol → RCl
168
+ rules.append(_make_reactor('hydroxy_to_chloro', 'tertiary_alcohol', 'alkyl_chloride', '[Cl:20]-[A:2]'))
169
+
170
+ # acid fluoride formation: RCOOH → RCOF (cyanuric fluoride, DAST)
171
+ rules.append(_make_reactor('acid_fluorination', 'carboxylic_acid', 'acyl_fluoride', '[F:20]-[A:1]=[A:2]'))
172
+
173
+ # Sandmeyer: ArNH2 → ArX (NaNO2/HX, CuX)
174
+ rules.append(_make_reactor('sandmeyer_bromination', 'primary_aniline', 'aryl_bromide', '[Br:20]-[A:2]'))
175
+ rules.append(_make_reactor('sandmeyer_iodination', 'primary_aniline', 'aryl_iodide', '[I:20]-[A:2]'))
176
+ rules.append(_make_reactor('sandmeyer_chlorination', 'primary_aniline', 'aryl_chloride', '[Cl:20]-[A:2]'))
177
+
178
+ # Oxo to difluoro: C=O → CF2 (DAST, DeoxoFluor)
179
+ rules.append(_make_reactor('oxo_to_difluoro', 'ketone', None, '[A:1](-[F:20])-[F:21]'))
180
+ rules.append(_make_reactor('oxo_to_difluoro', 'aldehyde', None, '[A:1](-[F:20])-[F:21]'))
181
+
182
+ # Oxo to thioxo: C=O → C=S (Lawesson's reagent, P4S10)
183
+ rules.append(_make_reactor('oxo_to_thioxo', 'primary_amide', 'thioamide', '[S:20]=[A:2]-[A:1]'))
184
+ rules.append(_make_reactor('oxo_to_thioxo', 'secondary_amide', None, '[S:20]=[A:2]-[A:1]'))
185
+ rules.append(_make_reactor('oxo_to_thioxo', 'ketone', None, '[S:20]=[A:1]'))
186
+
187
+ # Amino to isocyanate: ArNH2/RNH2 → ArNCO (triphosgene/COCl2)
188
+ # primary_aniline: [N:1][C;a:2] → need N=C=O, keep Ar bond
189
+ rules.append(_make_reactor('amino_to_isocyanate', 'primary_aniline', 'isocyanate', '[A:2]-[A:1]=[C:20]=[O:21]'))
190
+ rules.append(_make_reactor('amino_to_isocyanate', 'primary_amine', 'isocyanate', '[A:2]-[A:1]=[C:20]=[O:21]'))
191
+
192
+ # Amino to isothiocyanate: ArNH2/RNH2 → ArNCS (CS2/DCC, thiophosgene)
193
+ rules.append(_make_reactor('amino_to_isothiocyanate', 'primary_aniline', None, '[A:2]-[A:1]=[C:20]=[S:21]'))
194
+ rules.append(_make_reactor('amino_to_isothiocyanate', 'primary_amine', None, '[A:2]-[A:1]=[C:20]=[S:21]'))
195
+
196
+ # Amino to azide: ArNH2 → ArN3 (diazotization + NaN3)
197
+ rules.append(_make_reactor('amino_to_azide', 'primary_aniline', 'azide', '[A:2]-[N:20]=[N;+:21]=[N;-:22]'))
198
+ rules.append(_make_reactor('amino_to_azide', 'primary_amine', 'azide', '[A:2]-[N:20]=[N;+:21]=[N;-:22]'))
199
+
200
+ # Amino to hydroxy: ArNH2 → ArOH (diazotization + H2O)
201
+ rules.append(_make_reactor('amino_to_hydroxy', 'primary_aniline', 'phenol', '[A:2]-[O:20]'))
202
+ rules.append(_make_reactor('amino_to_hydroxy', 'primary_amine', 'primary_alcohol', '[A:2]-[O:20]'))
203
+
204
+ # Bpin hydrolysis: ArBpin → ArB(OH)2 (NaIO4, HCl, diethanolamine)
205
+ rules.append(_make_reactor('bpin_hydrolysis', 'aryl_boronic_ester', 'aryl_boronic_acid', '[A:1]-[B:20](-[O:21])-[O:22]'))
206
+ rules.append(_make_reactor('bpin_hydrolysis', 'alkyl_boronic_ester', 'alkyl_boronic_acid', '[A:1]-[B:20](-[O:21])-[O:22]'))
207
+
208
+ # Ester hydrolysis: RCOOR' → RCOOH (LiOH, NaOH, HCl)
209
+ rules.append(_make_reactor('ester_hydrolysis', 'ester', 'carboxylic_acid', '[A:1](=[A:2])-[O:20]'))
210
+
211
+ # Carboxy to amide: RCOOH → RCONH2 (EDC/HOBt+NH3, or CDI+NH3)
212
+ rules.append(_make_reactor('acid_to_amide', 'carboxylic_acid', 'primary_amide', '[N:20]-[A:1]=[A:2]'))
213
+
214
+ # Acyl chloride to acid: RCOCl → RCOOH (H2O)
215
+ rules.append(_make_reactor('acyl_chloride_hydrolysis', 'acyl_chloride', 'carboxylic_acid', '[O:20]-[A:1]=[A:2]'))
216
+ rules.append(_make_reactor('acyl_chloride_hydrolysis', 'acyl_fluoride', 'carboxylic_acid', '[O:20]-[A:1]=[A:2]'))
217
+
218
+ # Acyl chloride to amide: RCOCl → RCONH2 (NH3)
219
+ rules.append(_make_reactor('acyl_chloride_to_amide', 'acyl_chloride', 'primary_amide', '[N:20]-[A:1]=[A:2]'))
220
+ rules.append(_make_reactor('acyl_chloride_to_amide', 'acyl_fluoride', 'primary_amide', '[N:20]-[A:1]=[A:2]'))
221
+
222
+ # Halide to methoxy: ArX/RX → ArOMe/ROMe (NaOMe, SNAr)
223
+ rules.append(_make_reactor('halide_to_methoxy', 'aryl_fluoride', None, '[A:1]-[O:20]-[C:21]'))
224
+ rules.append(_make_reactor('halide_to_methoxy', 'aryl_chloride', None, '[A:1]-[O:20]-[C:21]'))
225
+ rules.append(_make_reactor('halide_to_methoxy', 'aryl_bromide', None, '[A:1]-[O:20]-[C:21]'))
226
+ rules.append(_make_reactor('halide_to_methoxy', 'alkyl_chloride', None, '[A:1]-[O:20]-[C:21]'))
227
+ rules.append(_make_reactor('halide_to_methoxy', 'alkyl_bromide', None, '[A:1]-[O:20]-[C:21]'))
228
+ rules.append(_make_reactor('halide_to_methoxy', 'alkyl_iodide', None, '[A:1]-[O:20]-[C:21]'))
229
+
230
+ # Halide to azide: RX → RN3 (NaN3)
231
+ rules.append(_make_reactor('azidation', 'alkyl_chloride', 'azide', '[A:1]-[N:20]=[N;+:21]=[N;-:22]'))
232
+ rules.append(_make_reactor('azidation', 'alkyl_bromide', 'azide', '[A:1]-[N:20]=[N;+:21]=[N;-:22]'))
233
+ rules.append(_make_reactor('azidation', 'alkyl_iodide', 'azide', '[A:1]-[N:20]=[N;+:21]=[N;-:22]'))
234
+ rules.append(_make_reactor('azidation', 'aryl_chloride', 'azide', '[A:1]-[N:20]=[N;+:21]=[N;-:22]'))
235
+ rules.append(_make_reactor('azidation', 'aryl_bromide', 'azide', '[A:1]-[N:20]=[N;+:21]=[N;-:22]'))
236
+ rules.append(_make_reactor('azidation', 'aryl_fluoride', 'azide', '[A:1]-[N:20]=[N;+:21]=[N;-:22]'))
237
+
238
+ # Hydroxy to azide: ROH → RN3 (DPPA/Mitsunobu or Ms→N3)
239
+ rules.append(_make_reactor('azidation', 'primary_alcohol', 'azide', '[A:2]-[N:20]=[N;+:21]=[N;-:22]'))
240
+ rules.append(_make_reactor('azidation', 'secondary_alcohol', 'azide', '[A:2]-[N:20]=[N;+:21]=[N;-:22]'))
241
+
242
+ # Halide exchange (Finkelstein): RCl/RBr → RI (NaI/acetone)
243
+ rules.append(_make_reactor('finkelstein', 'alkyl_chloride', 'alkyl_iodide', '[A:1]-[I:20]'))
244
+ rules.append(_make_reactor('finkelstein', 'alkyl_bromide', 'alkyl_iodide', '[A:1]-[I:20]'))
245
+ rules.append(_make_reactor('finkelstein', 'aryl_chloride', 'aryl_iodide', '[A:1]-[I:20]'))
246
+ rules.append(_make_reactor('finkelstein', 'aryl_bromide', 'aryl_iodide', '[A:1]-[I:20]'))
247
+
248
+ # Halex fluorination: ArCl/ArBr → ArF (KF, CsF, spray-dried KF)
249
+ rules.append(_make_reactor('halex', 'aryl_chloride', 'aryl_fluoride', '[A:1]-[F:20]'))
250
+ rules.append(_make_reactor('halex', 'aryl_bromide', 'aryl_fluoride', '[A:1]-[F:20]'))
251
+
252
+ # Halide to chloride: ArBr/ArI → ArCl; RI → RCl
253
+ rules.append(_make_reactor('halide_to_chloride', 'aryl_bromide', 'aryl_chloride', '[A:1]-[Cl:20]'))
254
+ rules.append(_make_reactor('halide_to_chloride', 'aryl_iodide', 'aryl_chloride', '[A:1]-[Cl:20]'))
255
+ rules.append(_make_reactor('halide_to_chloride', 'alkyl_iodide', 'alkyl_chloride', '[A:1]-[Cl:20]'))
256
+
257
+ # Halide to bromide: ArCl → ArBr (not common but exists)
258
+ rules.append(_make_reactor('halide_to_bromide', 'aryl_chloride', 'aryl_bromide', '[A:1]-[Br:20]'))
259
+
260
+ # Halide to thiol: RX → RSH (NaSH, thiourea hydrolysis)
261
+ rules.append(_make_reactor('halide_to_thiol', 'aryl_chloride', None, '[A:1]-[S:20]'))
262
+ rules.append(_make_reactor('halide_to_thiol', 'aryl_bromide', None, '[A:1]-[S:20]'))
263
+ rules.append(_make_reactor('halide_to_thiol', 'alkyl_bromide', 'thiol', '[A:1]-[S:20]'))
264
+ rules.append(_make_reactor('halide_to_thiol', 'alkyl_chloride', 'thiol', '[A:1]-[S:20]'))
265
+
266
+ # Halide to thioether: RX → RSMe (NaSMe)
267
+ rules.append(_make_reactor('halide_to_thioether', 'aryl_chloride', 'thioether', '[A:1]-[S:20]-[C:21]'))
268
+ rules.append(_make_reactor('halide_to_thioether', 'aryl_bromide', 'thioether', '[A:1]-[S:20]-[C:21]'))
269
+ rules.append(_make_reactor('halide_to_thioether', 'alkyl_bromide', 'thioether', '[A:1]-[S:20]-[C:21]'))
270
+ rules.append(_make_reactor('halide_to_thioether', 'alkyl_chloride', 'thioether', '[A:1]-[S:20]-[C:21]'))
271
+
272
+ return rules
273
+
274
+
275
+ rules = Proxy(_rules)
276
+
277
+
278
+ __all__ = ['rules']
@@ -0,0 +1,18 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 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,108 @@
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
+ import pytest
20
+ from chython import smiles
21
+ from chython.algorithms.groups._protective import rules
22
+
23
+
24
+ _test_cases = [(name, *rule) for name, rule in rules.items()]
25
+
26
+
27
+ @pytest.mark.parametrize('name,q,keep,add,test_smi,cleaved_smi,decoys', _test_cases, ids=[x[0] for x in _test_cases])
28
+ def test_deprotection(name, q, keep, add, test_smi, cleaved_smi, decoys):
29
+ t = smiles(test_smi)
30
+ t.canonicalize()
31
+ # query matches protected form
32
+ assert q < t, f'query does not match protected form'
33
+ # deprotection produces expected product
34
+ a = smiles(cleaved_smi)
35
+ a.canonicalize()
36
+ t.remove_protection(name)
37
+ assert t == a, f'deprotection gave {t}, expected {a}'
38
+ # decoys are not matched
39
+ for d in decoys:
40
+ d = smiles(d)
41
+ d.canonicalize()
42
+ assert not q < d, f'query matches decoy {d}'
43
+
44
+
45
+ def test_no_false_matches_from_ordering():
46
+ """Ensure general patterns come after specific ones to prevent false atom claims.
47
+
48
+ If pattern B matches within the protected molecule of pattern A, then A must come
49
+ BEFORE B in the rules dict (so A claims atoms first). This test fails when B comes
50
+ before A - indicating rules dict needs reordering.
51
+ """
52
+ names = list(rules.keys())
53
+ for name_a, (_, _, _, test_smi, *_) in rules.items():
54
+ mol = smiles(test_smi)
55
+ mol.canonicalize()
56
+ idx_a = names.index(name_a)
57
+ for name_b in names[:idx_a]: # patterns that come BEFORE name_a
58
+ q_b = rules[name_b][0]
59
+ if next(q_b.get_mapping(mol), None) is not None:
60
+ pytest.fail(
61
+ f"Pattern '{name_b}' (idx {names.index(name_b)}) matches within "
62
+ f"protected molecule of '{name_a}' (idx {idx_a}), but comes before it. "
63
+ f"Move '{name_b}' after '{name_a}' in _protective.py rules dict."
64
+ )
65
+
66
+
67
+ def test_protective_groups_no_overlap():
68
+ """Ensure protective_groups doesn't report false positives due to sub-pattern overlap.
69
+
70
+ Checks that deletable atoms (atoms not in keep list) of different detected PGs
71
+ don't overlap - matching the logic in the protective_groups property.
72
+ """
73
+ for name, (q, keep, add, test_smi, *_) in rules.items():
74
+ mol = smiles(test_smi)
75
+ mol.canonicalize()
76
+ pgs = mol.protective_groups
77
+ assert name in pgs, f'{name} not detected in its own protected SMILES'
78
+ # verify deletable atoms of detected PGs don't overlap
79
+ for other in pgs:
80
+ if other == name:
81
+ continue
82
+ q_other, keep_other = rules[other][0], rules[other][1]
83
+ q_self = rules[name][0]
84
+ self_atoms = set()
85
+ for mp in q_self.get_mapping(mol, automorphism_filter=False):
86
+ self_atoms.update(m for n, m in mp.items() if n not in keep)
87
+ for mp in q_other.get_mapping(mol, automorphism_filter=False):
88
+ other_atoms = {m for n, m in mp.items() if n not in keep_other}
89
+ assert not self_atoms.intersection(other_atoms), (
90
+ f"'{other}' deletable atoms overlap with '{name}' on '{test_smi}'. "
91
+ f"Reorder rules in _protective.py so '{name}' comes before '{other}'."
92
+ )
93
+
94
+
95
+ @pytest.mark.parametrize('smi,expected', [
96
+ ('CC(C)(C)OC(=O)N(C(=O)OC(C)(C)C)c1ccccc1', {'amine_boc': 2}),
97
+ ('CC(C)(C)OC(=O)Nc1ccc(OC(C)(C)C)cc1', {'amine_boc': 1, 'hydroxyl_tbu': 1}),
98
+ ('c1ccc(COC(=O)Nc2ccc(OCc3ccccc3)cc2)cc1', {'amine_cbz': 1, 'hydroxyl_benzyl': 1}),
99
+ ('CC(C)OCOCc1ccccc1', {'hydroxyl_bom': 1}),
100
+ ('C(OCOC)(OCOC)C', {'hydroxyl_mom': 2}),
101
+ ('OC(CO[Si](C)(C)C)O[Si](C)(C)C', {'hydroxyl_tms': 2}),
102
+ ], ids=['double_boc', 'boc_and_tbu', 'cbz_and_benzyl', 'bom_no_benzyl', 'double_mom', 'double_tms'])
103
+ def test_multiple_pg_on_molecule(smi, expected):
104
+ """Test correct detection of multiple PGs and no false positives from sub-pattern overlap."""
105
+ mol = smiles(smi)
106
+ mol.canonicalize()
107
+ pgs = mol.protective_groups
108
+ assert pgs == expected, f'got {pgs}'