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.
- chython/__init__.py +31 -0
- chython/_functions.py +69 -0
- chython/algorithms/__init__.py +18 -0
- chython/algorithms/_isomorphism.cp312-win_amd64.pyd +0 -0
- chython/algorithms/_isomorphism.pyx +189 -0
- chython/algorithms/aromatics/__init__.py +27 -0
- chython/algorithms/aromatics/_rules.py +124 -0
- chython/algorithms/aromatics/kekule.py +530 -0
- chython/algorithms/aromatics/test/__init__.py +18 -0
- chython/algorithms/aromatics/test/test_kekule.py +160 -0
- chython/algorithms/aromatics/test/test_thiele.py +263 -0
- chython/algorithms/aromatics/thiele.py +226 -0
- chython/algorithms/calculate2d/__init__.py +23 -0
- chython/algorithms/calculate2d/clean2d.js +1 -0
- chython/algorithms/calculate2d/molecule.py +149 -0
- chython/algorithms/calculate2d/reaction.py +80 -0
- chython/algorithms/depict.py +523 -0
- chython/algorithms/fingerprints/__init__.py +48 -0
- chython/algorithms/fingerprints/linear.py +208 -0
- chython/algorithms/fingerprints/morgan.py +137 -0
- chython/algorithms/fingerprints/test/__init__.py +18 -0
- chython/algorithms/fingerprints/test/test_linear.py +127 -0
- chython/algorithms/fingerprints/test/test_morgan.py +79 -0
- chython/algorithms/isomorphism.py +667 -0
- chython/algorithms/mapping/__init__.py +28 -0
- chython/algorithms/mapping/_groups.py +94 -0
- chython/algorithms/mapping/_reactions.py +72 -0
- chython/algorithms/mapping/attention.py +216 -0
- chython/algorithms/mapping/fixmapper.py +84 -0
- chython/algorithms/mapping/groups.py +137 -0
- chython/algorithms/mcs.py +202 -0
- chython/algorithms/morgan.py +82 -0
- chython/algorithms/rings.py +551 -0
- chython/algorithms/smiles.py +556 -0
- chython/algorithms/standardize/__init__.py +30 -0
- chython/algorithms/standardize/_charged.py +115 -0
- chython/algorithms/standardize/_groups.py +926 -0
- chython/algorithms/standardize/_metal_organics.py +189 -0
- chython/algorithms/standardize/_reagents.py +44 -0
- chython/algorithms/standardize/_salts.py +68 -0
- chython/algorithms/standardize/molecule.py +479 -0
- chython/algorithms/standardize/reaction.py +429 -0
- chython/algorithms/standardize/resonance.py +196 -0
- chython/algorithms/standardize/salts.py +128 -0
- chython/algorithms/standardize/saturation.py +378 -0
- chython/algorithms/standardize/test/__init__.py +18 -0
- chython/algorithms/standardize/test/test_groups.py +118 -0
- chython/algorithms/stereo.py +1222 -0
- chython/algorithms/tautomers/__init__.py +171 -0
- chython/algorithms/tautomers/_acid.py +59 -0
- chython/algorithms/tautomers/_base.py +116 -0
- chython/algorithms/tautomers/_keto_enol.py +107 -0
- chython/algorithms/tautomers/acid_base.py +214 -0
- chython/algorithms/tautomers/heteroarenes.py +108 -0
- chython/algorithms/tautomers/keto_enol.py +148 -0
- chython/algorithms/tautomers/test/__init__.py +18 -0
- chython/algorithms/tautomers/test/test_tautomers.py +80 -0
- chython/algorithms/test/__init__.py +18 -0
- chython/algorithms/test/test_isomorphism.py +58 -0
- chython/algorithms/test/test_smiles.py +76 -0
- chython/algorithms/x3dom.py +355 -0
- chython/containers/__init__.py +43 -0
- chython/containers/_pack_v2.cp312-win_amd64.pyd +0 -0
- chython/containers/_pack_v2.pyx +271 -0
- chython/containers/_unpack_v0v2.cp312-win_amd64.pyd +0 -0
- chython/containers/_unpack_v0v2.pyx +300 -0
- chython/containers/bonds.py +255 -0
- chython/containers/cgr.py +115 -0
- chython/containers/graph.py +200 -0
- chython/containers/molecule.py +695 -0
- chython/containers/query.py +66 -0
- chython/containers/reaction.py +324 -0
- chython/exceptions.py +114 -0
- chython/files/MRVrw.py +523 -0
- chython/files/PDBrw.py +257 -0
- chython/files/RDFrw.py +298 -0
- chython/files/SDFrw.py +221 -0
- chython/files/__init__.py +29 -0
- chython/files/_convert.py +207 -0
- chython/files/_mapping.py +117 -0
- chython/files/_xyz.cp312-win_amd64.pyd +0 -0
- chython/files/_xyz.pyx +74 -0
- chython/files/daylight/__init__.py +24 -0
- chython/files/daylight/parser.py +157 -0
- chython/files/daylight/smarts.py +107 -0
- chython/files/daylight/smiles.py +245 -0
- chython/files/daylight/test/__init__.py +18 -0
- chython/files/daylight/test/test_daylight_smarts.py +289 -0
- chython/files/daylight/test/test_daylight_smiles.py +101 -0
- chython/files/daylight/test/test_parser.py +118 -0
- chython/files/daylight/test/test_tokenize.py +77 -0
- chython/files/daylight/tokenize.py +376 -0
- chython/files/libinchi/__init__.py +22 -0
- chython/files/libinchi/libinchi.dll +0 -0
- chython/files/libinchi/wrapper.py +555 -0
- chython/files/mdl/__init__.py +25 -0
- chython/files/mdl/emol.py +209 -0
- chython/files/mdl/erxn.py +67 -0
- chython/files/mdl/mol.py +144 -0
- chython/files/mdl/read.py +225 -0
- chython/files/mdl/rxn.py +67 -0
- chython/files/mdl/stereo.py +63 -0
- chython/files/mdl/write.py +165 -0
- chython/files/xyz.py +78 -0
- chython/periodictable/__init__.py +69 -0
- chython/periodictable/base/__init__.py +25 -0
- chython/periodictable/base/dynamic.py +149 -0
- chython/periodictable/base/element.py +452 -0
- chython/periodictable/base/groups.py +90 -0
- chython/periodictable/base/periods.py +46 -0
- chython/periodictable/base/query.py +473 -0
- chython/periodictable/base/vector.py +119 -0
- chython/periodictable/groupI.py +252 -0
- chython/periodictable/groupII.py +223 -0
- chython/periodictable/groupIII.py +1154 -0
- chython/periodictable/groupIV.py +210 -0
- chython/periodictable/groupIX.py +196 -0
- chython/periodictable/groupV.py +192 -0
- chython/periodictable/groupVI.py +183 -0
- chython/periodictable/groupVII.py +162 -0
- chython/periodictable/groupVIII.py +161 -0
- chython/periodictable/groupX.py +166 -0
- chython/periodictable/groupXI.py +164 -0
- chython/periodictable/groupXII.py +158 -0
- chython/periodictable/groupXIII.py +239 -0
- chython/periodictable/groupXIV.py +262 -0
- chython/periodictable/groupXV.py +276 -0
- chython/periodictable/groupXVI.py +457 -0
- chython/periodictable/groupXVII.py +324 -0
- chython/periodictable/groupXVIII.py +260 -0
- chython/reactor/__init__.py +24 -0
- chython/reactor/base.py +212 -0
- chython/reactor/deprotection.py +565 -0
- chython/reactor/groups.py +128 -0
- chython/reactor/reactions/__init__.py +157 -0
- chython/reactor/reactions/_amidation.py +58 -0
- chython/reactor/reactions/_amine_isocyanate.py +52 -0
- chython/reactor/reactions/_buchwald_hartwig.py +50 -0
- chython/reactor/reactions/_esterification.py +49 -0
- chython/reactor/reactions/_macmillan.py +43 -0
- chython/reactor/reactions/_reductive_amination.py +50 -0
- chython/reactor/reactions/_sonogashira.py +49 -0
- chython/reactor/reactions/_sulfonamidation.py +52 -0
- chython/reactor/reactions/_suzuki_miyaura.py +88 -0
- chython/reactor/reactions/ufe.py +99 -0
- chython/reactor/reactor.py +170 -0
- chython/reactor/retro/__init__.py +77 -0
- chython/reactor/retro/_amidation.py +39 -0
- chython/reactor/retro/_aryl_amination.py +32 -0
- chython/reactor/retro/_mitsunobu.py +49 -0
- chython/reactor/retro/_sonogashira.py +49 -0
- chython/reactor/retro/_suzuki_miyaura.py +54 -0
- chython/reactor/scaffold.py +122 -0
- chython/reactor/test/__init__.py +18 -0
- chython/reactor/test/test_deprotection.py +57 -0
- chython/reactor/test/test_reactor.py +40 -0
- chython/reactor/test/test_scaffold.py +57 -0
- chython/reactor/test/test_transformer.py +40 -0
- chython/reactor/transformer.py +62 -0
- chython/utils/__init__.py +57 -0
- chython/utils/free_wilson.py +114 -0
- chython/utils/grid.py +130 -0
- chython/utils/rdkit.py +165 -0
- chython/utils/retro.py +139 -0
- chython/utils/svg.py +57 -0
- chython/utils/test/__init__.py +18 -0
- chython/utils/test/test_rdkit.py +71 -0
- chython-2.4.dist-info/LICENSE +165 -0
- chython-2.4.dist-info/METADATA +101 -0
- chython-2.4.dist-info/RECORD +171 -0
- 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}'
|