PFASGroups 3.2.2__py3-none-any.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.
- HalogenGroups/__init__.py +246 -0
- PFASGroups/ComponentsSolverModel.py +977 -0
- PFASGroups/HalogenGroupModel.py +810 -0
- PFASGroups/PFASDefinitionModel.py +393 -0
- PFASGroups/PFASEmbeddings.py +3315 -0
- PFASGroups/__init__.py +21 -0
- PFASGroups/cli.py +618 -0
- PFASGroups/core.py +415 -0
- PFASGroups/data/Halogen_groups_smarts.json +9024 -0
- PFASGroups/data/PFAS_definitions_smarts.json +170 -0
- PFASGroups/data/component_smarts.json +4 -0
- PFASGroups/data/component_smarts_halogens.json +142 -0
- PFASGroups/data/diatomic_bonds_dict.json +12802 -0
- PFASGroups/draw_mols.py +374 -0
- PFASGroups/embeddings.py +150 -0
- PFASGroups/fragmentation.py +548 -0
- PFASGroups/generate_homologues.py +256 -0
- PFASGroups/generate_mol.py +656 -0
- PFASGroups/generate_paper_figures.py +266 -0
- PFASGroups/getter.py +111 -0
- PFASGroups/homologue_series.py +473 -0
- PFASGroups/parser.py +942 -0
- PFASGroups/prioritise.py +439 -0
- pfasgroups-3.2.2.dist-info/METADATA +724 -0
- pfasgroups-3.2.2.dist-info/RECORD +28 -0
- pfasgroups-3.2.2.dist-info/WHEEL +5 -0
- pfasgroups-3.2.2.dist-info/entry_points.txt +3 -0
- pfasgroups-3.2.2.dist-info/top_level.txt +2 -0
PFASGroups/core.py
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import functools
|
|
4
|
+
import re
|
|
5
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
import networkx as nx
|
|
8
|
+
from rdkit import Chem
|
|
9
|
+
from rdkit import rdBase
|
|
10
|
+
|
|
11
|
+
# --- Load SMARTS paths from component_smarts.json ---
|
|
12
|
+
MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
DATA_DIR = os.path.join(MODULE_DIR, 'data')
|
|
14
|
+
HALOGEN_GROUPS_FILE = os.path.join(DATA_DIR, 'Halogen_groups_smarts.json')
|
|
15
|
+
PFAS_DEFINITIONS_FILE = os.path.join(DATA_DIR, 'PFAS_definitions_smarts.json')
|
|
16
|
+
COMPONENTS_FILE = os.path.join(DATA_DIR, 'component_smarts_halogens.json')
|
|
17
|
+
|
|
18
|
+
def rdkit_disable_log(level='warning'):
|
|
19
|
+
"""Disable RDKit warnings and errors logging to stderr"""
|
|
20
|
+
def disable_logs():
|
|
21
|
+
if level == 'error':
|
|
22
|
+
rdBase.DisableLog('rdApp.error')
|
|
23
|
+
rdBase.DisableLog('rdApp.warning')
|
|
24
|
+
elif level == 'warning':
|
|
25
|
+
rdBase.DisableLog('rdApp.warning')
|
|
26
|
+
else:
|
|
27
|
+
rdBase.DisableLog('rdApp.*')
|
|
28
|
+
def enable_logs():
|
|
29
|
+
rdBase.EnableLog('rdApp.*')
|
|
30
|
+
def inner(func):
|
|
31
|
+
@functools.wraps(func)
|
|
32
|
+
def wrapper(*args, **kwargs):
|
|
33
|
+
disable_logs()
|
|
34
|
+
func_ret = func(*args, **kwargs)
|
|
35
|
+
enable_logs()
|
|
36
|
+
return func_ret
|
|
37
|
+
return wrapper
|
|
38
|
+
return inner
|
|
39
|
+
rdBase.DisableLog('rdApp.error')
|
|
40
|
+
rdBase.DisableLog('rdApp.warning')
|
|
41
|
+
|
|
42
|
+
def remove_atoms(mol, idxs, removable = ['H','F','Cl','Br','I'], show_on_error = False):
|
|
43
|
+
"""Remove atoms by indices and maintain connectivity.
|
|
44
|
+
|
|
45
|
+
This function removes the specified atoms and their removable neighbors,
|
|
46
|
+
then reconnects the remaining structure to maintain molecular integrity.
|
|
47
|
+
"""
|
|
48
|
+
if not idxs:
|
|
49
|
+
return mol
|
|
50
|
+
|
|
51
|
+
to_remove = set()
|
|
52
|
+
# Map each removed atom to its non-removable neighbors
|
|
53
|
+
removed_to_neighbors = {}
|
|
54
|
+
|
|
55
|
+
# First pass: identify all atoms to remove and their connections
|
|
56
|
+
for idx in idxs:
|
|
57
|
+
atom = mol.GetAtomWithIdx(idx)
|
|
58
|
+
neighbors_r = [x.GetIdx() for x in atom.GetNeighbors() if x.GetSymbol() in removable]
|
|
59
|
+
neighbors_c = [x.GetIdx() for x in atom.GetNeighbors() if x.GetSymbol() not in removable]
|
|
60
|
+
|
|
61
|
+
# Add the atom and its removable neighbors to removal list
|
|
62
|
+
to_remove.add(idx)
|
|
63
|
+
to_remove.update(neighbors_r)
|
|
64
|
+
|
|
65
|
+
# Store non-removable neighbors for reconnection
|
|
66
|
+
if neighbors_c:
|
|
67
|
+
removed_to_neighbors[idx] = neighbors_c
|
|
68
|
+
|
|
69
|
+
# Build a graph of connectivity between removed atoms and their neighbors
|
|
70
|
+
# to determine how to reconnect the structure
|
|
71
|
+
new_bonds = []
|
|
72
|
+
processed_chains = set()
|
|
73
|
+
|
|
74
|
+
# Process each chain of consecutive removed atoms
|
|
75
|
+
for start_idx in idxs:
|
|
76
|
+
if start_idx in processed_chains:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# Find the chain of consecutive removed atoms containing start_idx
|
|
80
|
+
chain = [start_idx]
|
|
81
|
+
processed_chains.add(start_idx)
|
|
82
|
+
|
|
83
|
+
# Extend chain in both directions
|
|
84
|
+
queue = [start_idx]
|
|
85
|
+
while queue:
|
|
86
|
+
current = queue.pop(0)
|
|
87
|
+
for neighbor_idx in [n.GetIdx() for n in mol.GetAtomWithIdx(current).GetNeighbors()]:
|
|
88
|
+
if neighbor_idx in idxs and neighbor_idx not in processed_chains:
|
|
89
|
+
chain.append(neighbor_idx)
|
|
90
|
+
processed_chains.add(neighbor_idx)
|
|
91
|
+
queue.append(neighbor_idx)
|
|
92
|
+
|
|
93
|
+
# Find the endpoints of this chain (atoms that connect to non-removable parts)
|
|
94
|
+
chain_endpoints = []
|
|
95
|
+
for atom_idx in chain:
|
|
96
|
+
if atom_idx in removed_to_neighbors:
|
|
97
|
+
non_removable_neighbors = [n for n in removed_to_neighbors[atom_idx] if n not in to_remove]
|
|
98
|
+
if non_removable_neighbors:
|
|
99
|
+
chain_endpoints.extend(non_removable_neighbors)
|
|
100
|
+
|
|
101
|
+
# Connect the endpoints if there are exactly 2
|
|
102
|
+
if len(chain_endpoints) == 2 and chain_endpoints[0] != chain_endpoints[1]:
|
|
103
|
+
new_bonds.append((chain_endpoints[0], chain_endpoints[1]))
|
|
104
|
+
elif len(chain_endpoints) > 2:
|
|
105
|
+
# For branched structures, don't create connections that would change topology
|
|
106
|
+
# This prevents fragmentation but may not be chemically meaningful
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
# Create new molecule without removed atoms
|
|
110
|
+
rwm = Chem.RWMol()
|
|
111
|
+
_rwm = Chem.RWMol(mol)
|
|
112
|
+
Chem.Kekulize(_rwm)
|
|
113
|
+
|
|
114
|
+
# Map from old atom indices to new atom indices
|
|
115
|
+
old_to_new = {}
|
|
116
|
+
charged_atoms = []
|
|
117
|
+
|
|
118
|
+
# Add all atoms except those to be removed
|
|
119
|
+
for i, atom in enumerate(_rwm.GetAtoms()):
|
|
120
|
+
if i not in to_remove:
|
|
121
|
+
new_atom = Chem.Atom(atom.GetAtomicNum())
|
|
122
|
+
new_idx = rwm.AddAtom(new_atom)
|
|
123
|
+
# Copy formal charge if present
|
|
124
|
+
if atom.GetFormalCharge() != 0:
|
|
125
|
+
charged_atoms.append(atom.GetIdx())
|
|
126
|
+
old_to_new[i] = new_idx
|
|
127
|
+
|
|
128
|
+
# Copy existing bonds that don't involve removed atoms
|
|
129
|
+
for bond in _rwm.GetBonds():
|
|
130
|
+
a1 = bond.GetBeginAtomIdx()
|
|
131
|
+
a2 = bond.GetEndAtomIdx()
|
|
132
|
+
if a1 in old_to_new and a2 in old_to_new:
|
|
133
|
+
rwm.AddBond(old_to_new[a1], old_to_new[a2], bond.GetBondType())
|
|
134
|
+
|
|
135
|
+
# Add new bonds to maintain connectivity
|
|
136
|
+
for a, b in new_bonds:
|
|
137
|
+
if a in old_to_new and b in old_to_new and old_to_new[a] != old_to_new[b]:
|
|
138
|
+
# Check if bond already exists to avoid duplication
|
|
139
|
+
existing_bond = rwm.GetBondBetweenAtoms(old_to_new[a], old_to_new[b])
|
|
140
|
+
if existing_bond is None:
|
|
141
|
+
rwm.AddBond(old_to_new[a], old_to_new[b], Chem.BondType.SINGLE)
|
|
142
|
+
|
|
143
|
+
# Restore formal charges
|
|
144
|
+
for idx in charged_atoms:
|
|
145
|
+
if idx in old_to_new:
|
|
146
|
+
atom = rwm.GetAtomWithIdx(old_to_new[idx])
|
|
147
|
+
atom.SetFormalCharge(_rwm.GetAtomWithIdx(idx).GetFormalCharge())
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
Chem.SanitizeMol(rwm)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
if show_on_error is True:
|
|
153
|
+
_mol = rwm.GetMol()
|
|
154
|
+
try:
|
|
155
|
+
from .draw_mols import plot_mols # pylint: disable=import-outside-toplevel
|
|
156
|
+
img, _, _ = plot_mols([Chem.MolToSmiles(_mol)], subwidth=600, subheight=600, svg=False, addAtomIndices=True, bondLineWidth=0.5, fixedBondLength=15, minFontSize=12)
|
|
157
|
+
img.show()
|
|
158
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
159
|
+
pass
|
|
160
|
+
raise e
|
|
161
|
+
|
|
162
|
+
return rwm.GetMol()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def n_from_formula(formula:str, element=None)->Union[int,dict]:
|
|
166
|
+
"""
|
|
167
|
+
Compute the number of elements (any or one specific)
|
|
168
|
+
|
|
169
|
+
:params formula: Formula to parse
|
|
170
|
+
:params element: Element's symbol to find
|
|
171
|
+
:return: Number of elements in the formula
|
|
172
|
+
"""
|
|
173
|
+
if element is not None:
|
|
174
|
+
PAT = f"([{element}])"+r"(\d*)"
|
|
175
|
+
else:
|
|
176
|
+
PAT = r"([A-Z][a-z]?)(\d*)"
|
|
177
|
+
mat = re.findall(PAT,formula)
|
|
178
|
+
formula_dict = {}
|
|
179
|
+
for sym,nb in mat:
|
|
180
|
+
if nb != '':
|
|
181
|
+
formula_dict[sym] = formula_dict.setdefault(sym,0) + int(nb)
|
|
182
|
+
else:
|
|
183
|
+
formula_dict[sym] =formula_dict.setdefault(sym,0) + 1
|
|
184
|
+
if element is not None:
|
|
185
|
+
return formula_dict[element]
|
|
186
|
+
return formula_dict
|
|
187
|
+
|
|
188
|
+
def fragment_on_bond(mol, a1, a2):
|
|
189
|
+
"""Fragment a molecule on a bond between atoms a1 and a2 (indices)"""
|
|
190
|
+
bond = mol.GetBondBetweenAtoms(a1, a2)
|
|
191
|
+
mms = Chem.FragmentOnBonds(mol, [bond.GetIdx()], addDummies=False)
|
|
192
|
+
return [x for x in Chem.GetMolFrags(mms, asMols=True, sanitizeFrags = False)]
|
|
193
|
+
|
|
194
|
+
def fragment_until_valence_is_correct(mol, frags, verbose=False, _events=None):
|
|
195
|
+
"""Iterate over the molecule and fragment it until valence is corrected.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
mol : rdkit.Chem.Mol
|
|
200
|
+
Molecule to sanitize / fragment.
|
|
201
|
+
frags : list
|
|
202
|
+
Accumulator for sanitized fragment mols (pass ``[]`` on first call).
|
|
203
|
+
verbose : bool, optional
|
|
204
|
+
When *True* the function returns a 2-tuple ``(frags, events)`` instead
|
|
205
|
+
of just ``frags``. *events* is a list of dicts, one per fragmentation
|
|
206
|
+
step, each containing:
|
|
207
|
+
``atom_idx`` – index of the offending atom,
|
|
208
|
+
``error`` – the original :class:`~rdkit.Chem.AtomValenceException`
|
|
209
|
+
message,
|
|
210
|
+
``n_fragments`` – how many pieces the bond-cut produced,
|
|
211
|
+
``smiles`` – SMILES of the fragment being cut (may be ``None`` if
|
|
212
|
+
RDKit cannot generate one for an unsanitized mol).
|
|
213
|
+
_events : list or None
|
|
214
|
+
Internal accumulator used during recursion; do **not** pass explicitly.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
list or tuple
|
|
219
|
+
* ``verbose=False``: list of sanitized :class:`rdkit.Chem.Mol` fragments.
|
|
220
|
+
* ``verbose=True``: ``(frags, events)`` 2-tuple.
|
|
221
|
+
"""
|
|
222
|
+
if verbose and _events is None:
|
|
223
|
+
_events = []
|
|
224
|
+
try:
|
|
225
|
+
Chem.SanitizeMol(mol)
|
|
226
|
+
except (Chem.AtomValenceException, Chem.KekulizeException, Chem.AtomKekulizeException) as e:
|
|
227
|
+
e_str = str(e)
|
|
228
|
+
# Kekulization failures (KekulizeException / AtomKekulizeException) mean the ring
|
|
229
|
+
# system has no valid Kekulé form. Fragmenting on ring bonds does not help because
|
|
230
|
+
# the open-chain fragment retains AROMATIC bond types that still can't be kekulized.
|
|
231
|
+
# Instead, re-sanitize while skipping only the kekulization step so the molecule
|
|
232
|
+
# keeps its aromatic bonds — sufficient for SMARTS matching. If even that fails,
|
|
233
|
+
# skip the fragment entirely ("ignore the ring") rather than crashing.
|
|
234
|
+
if 'Kekulize' in type(e).__name__:
|
|
235
|
+
try:
|
|
236
|
+
_skip_kekule = Chem.SanitizeFlags.SANITIZE_ALL ^ Chem.SanitizeFlags.SANITIZE_KEKULIZE
|
|
237
|
+
Chem.SanitizeMol(mol, _skip_kekule)
|
|
238
|
+
if verbose:
|
|
239
|
+
return frags + [mol], _events
|
|
240
|
+
return frags + [mol]
|
|
241
|
+
except Exception:
|
|
242
|
+
# Truly unresolvable — skip this fragment silently
|
|
243
|
+
if verbose:
|
|
244
|
+
return frags, _events
|
|
245
|
+
return frags
|
|
246
|
+
# AtomValenceException format: "atom # 6 N, ..." → digit after "# "
|
|
247
|
+
all = [int(x) for x in re.findall(r"(?<=#\s)(\d+)", e_str)]
|
|
248
|
+
if not all:
|
|
249
|
+
# KekulizeException format: "Unkekulized atoms: 1 2 3 5 6"
|
|
250
|
+
if 'atoms:' in e_str:
|
|
251
|
+
all = [int(x) for x in re.findall(r'\d+', e_str.split('atoms:', 1)[-1])]
|
|
252
|
+
if len(all) == 0:
|
|
253
|
+
if verbose:
|
|
254
|
+
return frags, _events
|
|
255
|
+
raise e
|
|
256
|
+
neighbours = mol.GetAtomWithIdx(all[0]).GetNeighbors()
|
|
257
|
+
if not neighbours:
|
|
258
|
+
# Isolated atom with no bonds — cannot fragment; skip this fragment.
|
|
259
|
+
if verbose:
|
|
260
|
+
return frags, _events
|
|
261
|
+
return frags
|
|
262
|
+
bond_order_pairs = [(mol.GetBondBetweenAtoms(all[0], x.GetIdx()).GetBondType(), x.GetIdx()) for x in neighbours]
|
|
263
|
+
atom = sorted(bond_order_pairs, reverse=True)[0][1] # neighbour with bond of highest degree
|
|
264
|
+
mols = fragment_on_bond(mol, all[0], atom)
|
|
265
|
+
if verbose:
|
|
266
|
+
try:
|
|
267
|
+
smi = Chem.MolToSmiles(mol)
|
|
268
|
+
except Exception:
|
|
269
|
+
smi = None
|
|
270
|
+
_events.append({
|
|
271
|
+
'atom_idx': all[0],
|
|
272
|
+
'error': str(e),
|
|
273
|
+
'n_fragments': len(mols),
|
|
274
|
+
'smiles': smi,
|
|
275
|
+
})
|
|
276
|
+
for m in mols:
|
|
277
|
+
try:
|
|
278
|
+
if verbose:
|
|
279
|
+
frags, _events = fragment_until_valence_is_correct(m, frags, verbose=True, _events=_events)
|
|
280
|
+
else:
|
|
281
|
+
frags = fragment_until_valence_is_correct(m, frags)
|
|
282
|
+
except Exception:
|
|
283
|
+
# Fragment could not be processed (isolated atom, bad valence, etc.) — skip it.
|
|
284
|
+
if verbose:
|
|
285
|
+
return frags, _events
|
|
286
|
+
return frags
|
|
287
|
+
if verbose:
|
|
288
|
+
return frags, _events
|
|
289
|
+
return frags
|
|
290
|
+
else:
|
|
291
|
+
if verbose:
|
|
292
|
+
return frags + [mol], _events
|
|
293
|
+
return frags + [mol]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def mol_to_nx(mol):
|
|
297
|
+
"""Construct a networkx graph from a molecule"""
|
|
298
|
+
G = nx.Graph()
|
|
299
|
+
for n, atom in enumerate(mol.GetAtoms()):
|
|
300
|
+
element_Z = atom.GetAtomicNum()
|
|
301
|
+
node_params = {"element" : element_Z,
|
|
302
|
+
"symbol" : atom.GetSymbol()}
|
|
303
|
+
G.add_node(atom.GetIdx(),
|
|
304
|
+
**node_params
|
|
305
|
+
)
|
|
306
|
+
for bond in mol.GetBonds():
|
|
307
|
+
edgeOrder=bond.GetBondTypeAsDouble()
|
|
308
|
+
node1 = bond.GetBeginAtom()
|
|
309
|
+
node1 = node1.GetAtomicNum()
|
|
310
|
+
node2 = bond.GetEndAtom()
|
|
311
|
+
node2 = node2.GetAtomicNum()
|
|
312
|
+
G.add_edge(bond.GetBeginAtomIdx(),
|
|
313
|
+
bond.GetEndAtomIdx(),
|
|
314
|
+
order=edgeOrder)
|
|
315
|
+
return G
|
|
316
|
+
|
|
317
|
+
def get_substruct(_mol:Chem.Mol,struct:Chem.Mol):
|
|
318
|
+
"""Returns the indices of the atoms in the molecule that match the substructure"""
|
|
319
|
+
return set([x[0] for x in _mol.GetSubstructMatches(struct)])
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def preprocess_componentsSmarts(components):
|
|
323
|
+
"""Preprocess component SMARTS to be ready to use in the algorithm
|
|
324
|
+
Used by default for groups in Halogen_groups_smarts.json
|
|
325
|
+
In case you submit your own components, they should follow the same structure, and you should preprocess your dictionary with this function before passing it to parse_mol"""
|
|
326
|
+
_paths={}
|
|
327
|
+
for halogen, parts in components.items():
|
|
328
|
+
for form, saturations in parts.items():
|
|
329
|
+
for saturation, names in saturations.items():
|
|
330
|
+
s = names['component']
|
|
331
|
+
n = names['name']
|
|
332
|
+
smol = Chem.MolFromSmarts(s)
|
|
333
|
+
smol.UpdatePropertyCache()
|
|
334
|
+
Chem.GetSymmSSSR(smol)
|
|
335
|
+
smol.GetRingInfo().NumRings()
|
|
336
|
+
_paths[n] = {
|
|
337
|
+
"component": smol,
|
|
338
|
+
"halogen": halogen,
|
|
339
|
+
"form": form,
|
|
340
|
+
"saturation": saturation,
|
|
341
|
+
"constraints": names.get("constraints", {}),
|
|
342
|
+
}
|
|
343
|
+
return _paths
|
|
344
|
+
|
|
345
|
+
# --- Add SMARTS paths to function ---
|
|
346
|
+
def add_componentSmarts(filename = COMPONENTS_FILE):
|
|
347
|
+
"""Yields SMARTS for chains
|
|
348
|
+
|
|
349
|
+
Supports filtering by halogen, form, and saturation via kwargs:
|
|
350
|
+
- halogens: str or list of str, element symbols (e.g., 'F', ['F', 'Cl'])
|
|
351
|
+
- form: str or list of str, form types (e.g., 'alkyl', ['alkyl', 'cyclic'])
|
|
352
|
+
- saturation: str or list of str, saturation types (e.g., 'per', 'poly')
|
|
353
|
+
"""
|
|
354
|
+
paths = {}
|
|
355
|
+
with open(filename,'r') as f:
|
|
356
|
+
Hcomponents = json.load(f)
|
|
357
|
+
paths = preprocess_componentsSmarts(Hcomponents)
|
|
358
|
+
def inner(func):
|
|
359
|
+
def wrapper(*args,**kwargs):
|
|
360
|
+
# Extract filter parameters. Pop halogens so it doesn't conflict with
|
|
361
|
+
# inner function signatures, but restore it so load_componentsSolver
|
|
362
|
+
# can still read it after add_componentSmarts runs.
|
|
363
|
+
halogens = kwargs.pop('halogens', None)
|
|
364
|
+
form = kwargs.pop('form', None)
|
|
365
|
+
saturation = kwargs.pop('saturation', None)
|
|
366
|
+
# Restore halogens for downstream decorators (e.g. load_componentsSolver)
|
|
367
|
+
if halogens is not None:
|
|
368
|
+
kwargs['halogens'] = halogens
|
|
369
|
+
|
|
370
|
+
# Normalize filters to lists
|
|
371
|
+
if halogens is not None:
|
|
372
|
+
halogens = [halogens] if isinstance(halogens, str) else list(halogens)
|
|
373
|
+
if form is not None:
|
|
374
|
+
form = [form] if isinstance(form, str) else list(form)
|
|
375
|
+
if saturation is not None:
|
|
376
|
+
saturation = [saturation] if isinstance(saturation, str) else list(saturation)
|
|
377
|
+
|
|
378
|
+
# Filter paths if any filters are specified
|
|
379
|
+
filtered_paths = paths
|
|
380
|
+
if halogens or form or saturation:
|
|
381
|
+
filtered_paths = {}
|
|
382
|
+
for name, path_info in paths.items():
|
|
383
|
+
# Check halogen filter
|
|
384
|
+
if halogens is not None and path_info['halogen'] not in halogens:
|
|
385
|
+
continue
|
|
386
|
+
# Check form filter
|
|
387
|
+
if form is not None and path_info['form'] not in form:
|
|
388
|
+
continue
|
|
389
|
+
# Check saturation filter
|
|
390
|
+
if saturation is not None and path_info['saturation'] not in saturation:
|
|
391
|
+
continue
|
|
392
|
+
filtered_paths[name] = path_info
|
|
393
|
+
kwargs["componentSmartss"] = kwargs.get("componentSmartss", filtered_paths)
|
|
394
|
+
return func(*args, **kwargs)
|
|
395
|
+
return wrapper
|
|
396
|
+
return inner
|
|
397
|
+
|
|
398
|
+
def add_smarts(name = 'smarts'):
|
|
399
|
+
"""Yields preprocessed SMARTS to decorated function"""
|
|
400
|
+
smarts = {}
|
|
401
|
+
def add(s):
|
|
402
|
+
smol = Chem.MolFromSmarts(s)
|
|
403
|
+
smol.UpdatePropertyCache()
|
|
404
|
+
Chem.GetSymmSSSR(smol)
|
|
405
|
+
smol.GetRingInfo().NumRings()
|
|
406
|
+
smarts[s]=smol
|
|
407
|
+
return smol
|
|
408
|
+
def get(s):
|
|
409
|
+
return smarts.get(s,add(s))
|
|
410
|
+
def inner(func):
|
|
411
|
+
def wrapper(*args,**kwargs):
|
|
412
|
+
kwargs[name] = get(kwargs.get(name))
|
|
413
|
+
return func(*args, **kwargs)
|
|
414
|
+
return wrapper
|
|
415
|
+
return inner
|