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
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fragmentation utilities for HalogenGroups.
|
|
3
|
+
|
|
4
|
+
This module provides functions for fragmenting molecules based on bond dissociation energies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from rdkit import Chem
|
|
9
|
+
from rdkit.Chem.rdMolDescriptors import CalcMolFormula
|
|
10
|
+
from rdkit.Chem.Descriptors import ExactMolWt
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from itertools import combinations
|
|
14
|
+
|
|
15
|
+
def keysToInt(x):
|
|
16
|
+
"""Convert string keys to integers in a dictionary"""
|
|
17
|
+
return {int(k): v for k, v in x.items()}
|
|
18
|
+
|
|
19
|
+
def yield_scheme(name):
|
|
20
|
+
"""Yields SMARTS for chains"""
|
|
21
|
+
if name == 'BDE':
|
|
22
|
+
# For standalone use, we would need to provide the bond dissociation energy data
|
|
23
|
+
# This is a simplified version that creates a basic BDE dictionary
|
|
24
|
+
# In a full implementation, this would load from a data file
|
|
25
|
+
data = {
|
|
26
|
+
6: {6: 346, 1: 414, 9: 485, 17: 339, 35: 285}, # Carbon bonds
|
|
27
|
+
1: {6: 414, 1: 436, 9: 568, 17: 431, 35: 366}, # Hydrogen bonds
|
|
28
|
+
9: {6: 485, 1: 568, 9: 159}, # Fluorine bonds
|
|
29
|
+
17: {6: 339, 1: 431, 17: 243}, # Chlorine bonds
|
|
30
|
+
35: {6: 285, 1: 366, 35: 193}, # Bromine bonds
|
|
31
|
+
}
|
|
32
|
+
# Make symmetric
|
|
33
|
+
for atom1 in data:
|
|
34
|
+
for atom2 in data[atom1]:
|
|
35
|
+
if atom2 not in data:
|
|
36
|
+
data[atom2] = {}
|
|
37
|
+
data[atom2][atom1] = data[atom1][atom2]
|
|
38
|
+
else:
|
|
39
|
+
raise ValueError(f"Yield Scheme not implemented for {name}")
|
|
40
|
+
|
|
41
|
+
def inner(func):
|
|
42
|
+
def wrapper(*args,**kwargs):
|
|
43
|
+
kwargs["bondWeights"] = data
|
|
44
|
+
return func(*args, **kwargs)
|
|
45
|
+
return wrapper
|
|
46
|
+
return inner
|
|
47
|
+
|
|
48
|
+
def get_fragments(mol, idx, all=False):
|
|
49
|
+
"""Fragment a molecule by breaking specified bonds"""
|
|
50
|
+
frags = []
|
|
51
|
+
if all is True:
|
|
52
|
+
with Chem.RWMol(mol) as rwmol:
|
|
53
|
+
for b_idx in idx:
|
|
54
|
+
b = rwmol.GetBondWithIdx(b_idx)
|
|
55
|
+
rwmol.RemoveBond(b.GetBeginAtomIdx(), b.GetEndAtomIdx())
|
|
56
|
+
frags = frags + [x for x in Chem.GetMolFrags(rwmol, asMols=True)]
|
|
57
|
+
else:
|
|
58
|
+
for b_idx in idx:
|
|
59
|
+
with Chem.RWMol(mol) as rwmol:
|
|
60
|
+
b = rwmol.GetBondWithIdx(b_idx)
|
|
61
|
+
rwmol.RemoveBond(b.GetBeginAtomIdx(), b.GetEndAtomIdx())
|
|
62
|
+
frags = frags + [x for x in Chem.GetMolFrags(rwmol, asMols=True)]
|
|
63
|
+
return frags
|
|
64
|
+
|
|
65
|
+
@yield_scheme('BDE')
|
|
66
|
+
def generate_fragments(mol, bondWeights=None, nb_breakingpoints=5):
|
|
67
|
+
"""Break molecule on weakest bonds using BDE"""
|
|
68
|
+
bonds = []
|
|
69
|
+
strengths = []
|
|
70
|
+
for i, bond in enumerate(mol.GetBonds()):
|
|
71
|
+
node1 = bond.GetBeginAtom().GetAtomicNum()
|
|
72
|
+
node2 = bond.GetEndAtom().GetAtomicNum()
|
|
73
|
+
try:
|
|
74
|
+
value = bondWeights[node1][node2]
|
|
75
|
+
except:
|
|
76
|
+
try:
|
|
77
|
+
value = bondWeights[node2][node1]
|
|
78
|
+
except:
|
|
79
|
+
value = 300 # Default bond strength
|
|
80
|
+
bonds.append(i)
|
|
81
|
+
strengths.append(value)
|
|
82
|
+
|
|
83
|
+
# Invert strengths for probability calculation (weaker bonds more likely to break)
|
|
84
|
+
inv_strengths = [1.0/s for s in strengths]
|
|
85
|
+
prob = np.array(inv_strengths) / np.sum(inv_strengths)
|
|
86
|
+
|
|
87
|
+
fragments = []
|
|
88
|
+
breaking_points_idx = np.random.choice(range(len(bonds)),
|
|
89
|
+
size=min(nb_breakingpoints, len(bonds)-1),
|
|
90
|
+
replace=False, p=prob)
|
|
91
|
+
frags = get_fragments(mol, [bonds[i] for i in breaking_points_idx])
|
|
92
|
+
fragments += frags
|
|
93
|
+
return fragments
|
|
94
|
+
|
|
95
|
+
def fragment(mol, bde_dict, max_depth=4, nb_breakingpoints=5):
|
|
96
|
+
"""Fragment a molecule into smaller fragments by breaking the weakest bonds.
|
|
97
|
+
mol: rdkit.Chem.Mol object
|
|
98
|
+
bde_dict: dictionary with bond dissociation energies"""
|
|
99
|
+
if not isinstance(mol, Chem.Mol):
|
|
100
|
+
raise TypeError("Input mol must be an rdkit.Chem.Mol object")
|
|
101
|
+
|
|
102
|
+
fragments = []
|
|
103
|
+
process_fragments = [mol]
|
|
104
|
+
|
|
105
|
+
for i in range(max_depth):
|
|
106
|
+
next_fragments = []
|
|
107
|
+
for frag in process_fragments:
|
|
108
|
+
if len(frag.GetBonds()) > 1:
|
|
109
|
+
frags = generate_fragments(frag, bondWeights=bde_dict, nb_breakingpoints=nb_breakingpoints)
|
|
110
|
+
fragments = fragments + frags
|
|
111
|
+
next_fragments = next_fragments + frags
|
|
112
|
+
process_fragments = next_fragments
|
|
113
|
+
|
|
114
|
+
fragments_d = {}
|
|
115
|
+
for frag in fragments:
|
|
116
|
+
try:
|
|
117
|
+
inchi_key = Chem.MolToInchiKey(frag)
|
|
118
|
+
formula = CalcMolFormula(frag)
|
|
119
|
+
if inchi_key not in fragments_d:
|
|
120
|
+
fragments_d[inchi_key] = {}
|
|
121
|
+
if formula not in fragments_d[inchi_key]:
|
|
122
|
+
fragments_d[inchi_key][formula] = {'n': 0, 'mol': frag}
|
|
123
|
+
fragments_d[inchi_key][formula]['n'] += 1
|
|
124
|
+
except:
|
|
125
|
+
# Skip problematic fragments
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
return fragments_d
|
|
129
|
+
|
|
130
|
+
def fragment_to_distribution(mol, bde_dict, max_depth=4, nb_breakingpoints=5):
|
|
131
|
+
"""Fragment a molecule into smaller fragments and return mass spectrum-like distribution.
|
|
132
|
+
mol: rdkit.Chem.Mol object
|
|
133
|
+
bde_dict: dictionary with bond dissociation energies"""
|
|
134
|
+
if not isinstance(mol, Chem.Mol):
|
|
135
|
+
raise TypeError("Input mol must be an rdkit.Chem.Mol object")
|
|
136
|
+
|
|
137
|
+
fragments = fragment(mol, bde_dict, max_depth=max_depth, nb_breakingpoints=nb_breakingpoints)
|
|
138
|
+
intensities = {}
|
|
139
|
+
|
|
140
|
+
for key, value in fragments.items():
|
|
141
|
+
for formula, data in value.items():
|
|
142
|
+
try:
|
|
143
|
+
molw = ExactMolWt(data['mol'])
|
|
144
|
+
if molw not in intensities:
|
|
145
|
+
intensities[molw] = 0
|
|
146
|
+
intensities[molw] += data['n']
|
|
147
|
+
except:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
mz = list(intensities.keys())
|
|
151
|
+
intensity = list(intensities.values())
|
|
152
|
+
|
|
153
|
+
if len(intensity) > 0:
|
|
154
|
+
intensity_distribution = np.array(intensity) / np.sum(intensity)
|
|
155
|
+
sorted_indices = np.argsort(mz)
|
|
156
|
+
mz = np.array(mz)[sorted_indices]
|
|
157
|
+
intensity_distribution = intensity_distribution[sorted_indices]
|
|
158
|
+
return mz, intensity_distribution
|
|
159
|
+
else:
|
|
160
|
+
return np.array([]), np.array([])
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def find_fluorinated_chains(mol, componentSmartsName='Perfluoroalkyl'):
|
|
164
|
+
"""
|
|
165
|
+
Find fluorinated chains in a molecule using SMARTS patterns.
|
|
166
|
+
Returns atom indices that are part of fluorinated chains.
|
|
167
|
+
"""
|
|
168
|
+
# Load SMARTS paths from component_smarts.json
|
|
169
|
+
MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
170
|
+
DATA_DIR = os.path.join(MODULE_DIR, 'data')
|
|
171
|
+
COMPONENTS_SMARTS_FILE = os.path.join(DATA_DIR, 'component_smarts.json')
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
with open(COMPONENTS_SMARTS_FILE, 'r') as f:
|
|
175
|
+
component_smarts = json.load(f)
|
|
176
|
+
except FileNotFoundError:
|
|
177
|
+
# Fallback SMARTS patterns if file not found
|
|
178
|
+
component_smarts = {
|
|
179
|
+
'Perfluoroalkyl': '[#6]([#9])([#9])[#6]([#9])([#9])',
|
|
180
|
+
'Perfluoroalkyl_end': '[#6]([#9])([#9])([#6,#9,#17,#35,#53])',
|
|
181
|
+
'Polyfluoroalkyl': '[#6]([#9,#1,#17,#35,#53])([#9,#1,#17,#35,#53])[#6]([#9,#1,#17,#35,#53])([#9,#1,#17,#35,#53])',
|
|
182
|
+
'Polyfluoroalkyl_end': '[#6]([#9,#1,#17,#35,#53])([#9,#1,#17,#35,#53])([#6,#9,#1,#17,#35,#53])'
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Get SMARTS patterns
|
|
186
|
+
path_smarts = component_smarts.get(componentSmartsName)
|
|
187
|
+
end_smarts = component_smarts.get(componentSmartsName + '_end')
|
|
188
|
+
|
|
189
|
+
if not path_smarts or not end_smarts:
|
|
190
|
+
return set()
|
|
191
|
+
|
|
192
|
+
# Convert SMARTS to molecules
|
|
193
|
+
try:
|
|
194
|
+
path_mol = Chem.MolFromSmarts(path_smarts)
|
|
195
|
+
end_mol = Chem.MolFromSmarts(end_smarts)
|
|
196
|
+
except:
|
|
197
|
+
return set()
|
|
198
|
+
|
|
199
|
+
if not path_mol or not end_mol:
|
|
200
|
+
return set()
|
|
201
|
+
|
|
202
|
+
# Find all atoms that are part of fluorinated chains
|
|
203
|
+
fluorinated_atoms = set()
|
|
204
|
+
|
|
205
|
+
# Get all matches for path and end patterns
|
|
206
|
+
path_matches = mol.GetSubstructMatches(path_mol)
|
|
207
|
+
end_matches = mol.GetSubstructMatches(end_mol)
|
|
208
|
+
|
|
209
|
+
# Add all atoms from path matches
|
|
210
|
+
for match in path_matches:
|
|
211
|
+
fluorinated_atoms.update(match)
|
|
212
|
+
|
|
213
|
+
# Add all atoms from end matches
|
|
214
|
+
for match in end_matches:
|
|
215
|
+
fluorinated_atoms.update(match)
|
|
216
|
+
|
|
217
|
+
return fluorinated_atoms
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_non_fluorinated_bonds(mol, fluorinated_atoms=None, componentSmartsName='Perfluoroalkyl'):
|
|
221
|
+
"""
|
|
222
|
+
Get bond indices that are not part of the fluorinated chain.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
mol: RDKit molecule object
|
|
226
|
+
fluorinated_atoms: Set of atom indices that are part of fluorinated chains
|
|
227
|
+
componentSmartsName: Name of the SMARTS pattern to use for identifying fluorinated chains
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of bond indices that are not in the fluorinated chain
|
|
231
|
+
"""
|
|
232
|
+
if fluorinated_atoms is None:
|
|
233
|
+
fluorinated_atoms = find_fluorinated_chains(mol, componentSmartsName)
|
|
234
|
+
|
|
235
|
+
non_fluorinated_bonds = []
|
|
236
|
+
|
|
237
|
+
for bond in mol.GetBonds():
|
|
238
|
+
begin_idx = bond.GetBeginAtomIdx()
|
|
239
|
+
end_idx = bond.GetEndAtomIdx()
|
|
240
|
+
|
|
241
|
+
# If both atoms are in the fluorinated chain, keep the bond intact
|
|
242
|
+
if begin_idx in fluorinated_atoms and end_idx in fluorinated_atoms:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
# If bond connects fluorinated chain to other parts, it can be broken
|
|
246
|
+
# If bond is between non-fluorinated atoms, it can be broken
|
|
247
|
+
non_fluorinated_bonds.append(bond.GetIdx())
|
|
248
|
+
|
|
249
|
+
return non_fluorinated_bonds
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def get_bonds_connected_to_fluorinated_path(mol, fluorinated_atoms=None, componentSmartsName='Perfluoroalkyl'):
|
|
253
|
+
"""
|
|
254
|
+
Get bond indices that are connected to (but not part of) the fluorinated path.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
mol: RDKit molecule object
|
|
258
|
+
fluorinated_atoms: Set of atom indices that are part of fluorinated chains
|
|
259
|
+
componentSmartsName: Name of the SMARTS pattern to use for identifying fluorinated chains
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
List of bond indices that are connected to but not part of the fluorinated chain
|
|
263
|
+
"""
|
|
264
|
+
if fluorinated_atoms is None:
|
|
265
|
+
fluorinated_atoms = find_fluorinated_chains(mol, componentSmartsName)
|
|
266
|
+
|
|
267
|
+
connected_bonds = []
|
|
268
|
+
|
|
269
|
+
for bond in mol.GetBonds():
|
|
270
|
+
begin_idx = bond.GetBeginAtomIdx()
|
|
271
|
+
end_idx = bond.GetEndAtomIdx()
|
|
272
|
+
|
|
273
|
+
# Skip bonds that are entirely within the fluorinated chain
|
|
274
|
+
if begin_idx in fluorinated_atoms and end_idx in fluorinated_atoms:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
# Only include bonds where exactly one atom is in the fluorinated chain
|
|
278
|
+
# This ensures we're breaking bonds that connect the fluorinated path to other parts
|
|
279
|
+
if (begin_idx in fluorinated_atoms and end_idx not in fluorinated_atoms) or \
|
|
280
|
+
(begin_idx not in fluorinated_atoms and end_idx in fluorinated_atoms):
|
|
281
|
+
connected_bonds.append(bond.GetIdx())
|
|
282
|
+
|
|
283
|
+
return connected_bonds
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def generate_degradation_products(mol, componentSmartsName='Perfluoroalkyl', max_breaks=3,
|
|
287
|
+
min_fragment_size=2, include_original=True):
|
|
288
|
+
"""
|
|
289
|
+
Generate degradation products by breaking bonds that are connected to but not part of the fluorinated chain.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
mol: RDKit molecule object
|
|
293
|
+
componentSmartsName: Name of the SMARTS pattern to use for identifying fluorinated chains
|
|
294
|
+
max_breaks: Maximum number of bonds to break simultaneously
|
|
295
|
+
min_fragment_size: Minimum number of atoms in a fragment to keep it
|
|
296
|
+
include_original: Whether to include the original molecule in results
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Dictionary with InChI keys mapping to fragment information
|
|
300
|
+
"""
|
|
301
|
+
if not isinstance(mol, Chem.Mol):
|
|
302
|
+
raise TypeError("Input mol must be an rdkit.Chem.Mol object")
|
|
303
|
+
|
|
304
|
+
# Find fluorinated chain atoms
|
|
305
|
+
fluorinated_atoms = find_fluorinated_chains(mol, componentSmartsName)
|
|
306
|
+
|
|
307
|
+
# Get bonds that can be broken (connected to but not part of fluorinated chain)
|
|
308
|
+
breakable_bonds = get_bonds_connected_to_fluorinated_path(mol, fluorinated_atoms, componentSmartsName)
|
|
309
|
+
|
|
310
|
+
if not breakable_bonds:
|
|
311
|
+
# No breakable bonds found, return original molecule
|
|
312
|
+
if include_original:
|
|
313
|
+
inchi_key = Chem.MolToInchiKey(mol)
|
|
314
|
+
formula = CalcMolFormula(mol)
|
|
315
|
+
return {inchi_key: {formula: {'mol': mol, 'n_breaks': 0, 'broken_bonds': []}}}
|
|
316
|
+
else:
|
|
317
|
+
return {}
|
|
318
|
+
|
|
319
|
+
degradation_products = {}
|
|
320
|
+
|
|
321
|
+
# Include original molecule if requested
|
|
322
|
+
if include_original:
|
|
323
|
+
try:
|
|
324
|
+
inchi_key = Chem.MolToInchiKey(mol)
|
|
325
|
+
formula = CalcMolFormula(mol)
|
|
326
|
+
degradation_products[inchi_key] = {
|
|
327
|
+
formula: {'mol': mol, 'n_breaks': 0, 'broken_bonds': []}
|
|
328
|
+
}
|
|
329
|
+
except:
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
# Generate all possible combinations of bond breaks
|
|
333
|
+
for n_breaks in range(1, min(max_breaks + 1, len(breakable_bonds) + 1)):
|
|
334
|
+
for bond_combination in combinations(breakable_bonds, n_breaks):
|
|
335
|
+
try:
|
|
336
|
+
# Break the bonds
|
|
337
|
+
rwmol = Chem.RWMol(mol)
|
|
338
|
+
|
|
339
|
+
# Sort bonds in descending order to avoid index shifting issues
|
|
340
|
+
sorted_bonds = sorted(bond_combination, reverse=True)
|
|
341
|
+
|
|
342
|
+
for bond_idx in sorted_bonds:
|
|
343
|
+
bond = rwmol.GetBondWithIdx(bond_idx)
|
|
344
|
+
rwmol.RemoveBond(bond.GetBeginAtomIdx(), bond.GetEndAtomIdx())
|
|
345
|
+
|
|
346
|
+
# Get fragments
|
|
347
|
+
fragments = Chem.GetMolFrags(rwmol, asMols=True, sanitizeFrags=True)
|
|
348
|
+
|
|
349
|
+
# Process each fragment
|
|
350
|
+
for frag in fragments:
|
|
351
|
+
# Skip fragments that are too small
|
|
352
|
+
if frag.GetNumAtoms() < min_fragment_size:
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
inchi_key = Chem.MolToInchiKey(frag)
|
|
357
|
+
formula = CalcMolFormula(frag)
|
|
358
|
+
|
|
359
|
+
if inchi_key not in degradation_products:
|
|
360
|
+
degradation_products[inchi_key] = {}
|
|
361
|
+
|
|
362
|
+
if formula not in degradation_products[inchi_key]:
|
|
363
|
+
degradation_products[inchi_key][formula] = {
|
|
364
|
+
'mol': frag,
|
|
365
|
+
'n_breaks': n_breaks,
|
|
366
|
+
'broken_bonds': list(bond_combination)
|
|
367
|
+
}
|
|
368
|
+
except:
|
|
369
|
+
# Skip problematic fragments
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
except Exception as e:
|
|
373
|
+
# Skip problematic bond breaking combinations
|
|
374
|
+
continue
|
|
375
|
+
|
|
376
|
+
return degradation_products
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def generate_systematic_degradation_products(mol, componentSmartsName='Perfluoroalkyl',
|
|
380
|
+
preserve_fluorinated_chain=True,
|
|
381
|
+
min_fragment_size=2,
|
|
382
|
+
max_combinations=1000):
|
|
383
|
+
"""
|
|
384
|
+
Generate systematic degradation products by exploring all possible bond breaking patterns.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
mol: RDKit molecule object
|
|
388
|
+
componentSmartsName: Name of the SMARTS pattern to use for identifying fluorinated chains
|
|
389
|
+
preserve_fluorinated_chain: If True, only break bonds connected to (not part of) fluorinated chain
|
|
390
|
+
min_fragment_size: Minimum number of atoms in a fragment to keep it
|
|
391
|
+
max_combinations: Maximum number of bond combinations to explore
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Dictionary with degradation products organized by number of breaks
|
|
395
|
+
"""
|
|
396
|
+
if not isinstance(mol, Chem.Mol):
|
|
397
|
+
raise TypeError("Input mol must be an rdkit.Chem.Mol object")
|
|
398
|
+
|
|
399
|
+
# Find fluorinated chain atoms
|
|
400
|
+
fluorinated_atoms = find_fluorinated_chains(mol, componentSmartsName)
|
|
401
|
+
|
|
402
|
+
# Get all bonds in the molecule
|
|
403
|
+
all_bonds = list(range(mol.GetNumBonds()))
|
|
404
|
+
|
|
405
|
+
if preserve_fluorinated_chain:
|
|
406
|
+
# Only consider bonds connected to but not part of the fluorinated chain
|
|
407
|
+
breakable_bonds = get_bonds_connected_to_fluorinated_path(mol, fluorinated_atoms, componentSmartsName)
|
|
408
|
+
else:
|
|
409
|
+
# Consider all bonds
|
|
410
|
+
breakable_bonds = all_bonds
|
|
411
|
+
|
|
412
|
+
if not breakable_bonds:
|
|
413
|
+
return {"0_breaks": {Chem.MolToInchiKey(mol): {CalcMolFormula(mol): mol}}}
|
|
414
|
+
|
|
415
|
+
degradation_products = {}
|
|
416
|
+
combinations_explored = 0
|
|
417
|
+
|
|
418
|
+
# Explore different numbers of breaks
|
|
419
|
+
for n_breaks in range(len(breakable_bonds) + 1):
|
|
420
|
+
degradation_products[f"{n_breaks}_breaks"] = {}
|
|
421
|
+
|
|
422
|
+
if n_breaks == 0:
|
|
423
|
+
# Original molecule
|
|
424
|
+
try:
|
|
425
|
+
inchi_key = Chem.MolToInchiKey(mol)
|
|
426
|
+
formula = CalcMolFormula(mol)
|
|
427
|
+
degradation_products[f"{n_breaks}_breaks"][inchi_key] = {formula: mol}
|
|
428
|
+
except:
|
|
429
|
+
pass
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
# Generate combinations of bonds to break
|
|
433
|
+
for bond_combination in combinations(breakable_bonds, n_breaks):
|
|
434
|
+
if combinations_explored >= max_combinations:
|
|
435
|
+
break
|
|
436
|
+
|
|
437
|
+
combinations_explored += 1
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
# Create a copy and break the bonds
|
|
441
|
+
rwmol = Chem.RWMol(mol)
|
|
442
|
+
|
|
443
|
+
# Sort bonds in descending order to avoid index shifting
|
|
444
|
+
sorted_bonds = sorted(bond_combination, reverse=True)
|
|
445
|
+
|
|
446
|
+
for bond_idx in sorted_bonds:
|
|
447
|
+
bond = rwmol.GetBondWithIdx(bond_idx)
|
|
448
|
+
rwmol.RemoveBond(bond.GetBeginAtomIdx(), bond.GetEndAtomIdx())
|
|
449
|
+
|
|
450
|
+
# Get fragments
|
|
451
|
+
fragments = Chem.GetMolFrags(rwmol, asMols=True, sanitizeFrags=True)
|
|
452
|
+
|
|
453
|
+
# Store each fragment
|
|
454
|
+
for frag in fragments:
|
|
455
|
+
if frag.GetNumAtoms() < min_fragment_size:
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
inchi_key = Chem.MolToInchiKey(frag)
|
|
460
|
+
formula = CalcMolFormula(frag)
|
|
461
|
+
|
|
462
|
+
if inchi_key not in degradation_products[f"{n_breaks}_breaks"]:
|
|
463
|
+
degradation_products[f"{n_breaks}_breaks"][inchi_key] = {}
|
|
464
|
+
|
|
465
|
+
degradation_products[f"{n_breaks}_breaks"][inchi_key][formula] = frag
|
|
466
|
+
|
|
467
|
+
except:
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
except:
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
if combinations_explored >= max_combinations:
|
|
474
|
+
break
|
|
475
|
+
|
|
476
|
+
return degradation_products
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def analyse_degradation_pathways(mol, componentSmartsName='Perfluoroalkyl', max_breaks=3):
|
|
480
|
+
"""
|
|
481
|
+
Analyse possible degradation pathways and return detailed information about fragments.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
mol: RDKit molecule object
|
|
485
|
+
componentSmartsName: Name of the SMARTS pattern to use for identifying fluorinated chains
|
|
486
|
+
max_breaks: Maximum number of bonds to break
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Dictionary with detailed pathway analysis
|
|
490
|
+
"""
|
|
491
|
+
if not isinstance(mol, Chem.Mol):
|
|
492
|
+
raise TypeError("Input mol must be an rdkit.Chem.Mol object")
|
|
493
|
+
|
|
494
|
+
# Get degradation products
|
|
495
|
+
degradation_products = generate_degradation_products(
|
|
496
|
+
mol, componentSmartsName=componentSmartsName, max_breaks=max_breaks
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# Analyze the results
|
|
500
|
+
analysis = {
|
|
501
|
+
'original_molecule': {
|
|
502
|
+
'smiles': Chem.MolToSmiles(mol),
|
|
503
|
+
'formula': CalcMolFormula(mol),
|
|
504
|
+
'molecular_weight': ExactMolWt(mol),
|
|
505
|
+
'fluorinated_atoms': len(find_fluorinated_chains(mol, componentSmartsName))
|
|
506
|
+
},
|
|
507
|
+
'degradation_summary': {
|
|
508
|
+
'total_products': len(degradation_products),
|
|
509
|
+
'products_by_breaks': {}
|
|
510
|
+
},
|
|
511
|
+
'products': []
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
# Organize products by number of breaks
|
|
515
|
+
for inchi_key, formulas in degradation_products.items():
|
|
516
|
+
for formula, data in formulas.items():
|
|
517
|
+
frag_mol = data['mol']
|
|
518
|
+
n_breaks = data['n_breaks']
|
|
519
|
+
|
|
520
|
+
if n_breaks not in analysis['degradation_summary']['products_by_breaks']:
|
|
521
|
+
analysis['degradation_summary']['products_by_breaks'][n_breaks] = 0
|
|
522
|
+
analysis['degradation_summary']['products_by_breaks'][n_breaks] += 1
|
|
523
|
+
|
|
524
|
+
# Analyze fragment
|
|
525
|
+
frag_fluorinated_atoms = find_fluorinated_chains(frag_mol, componentSmartsName)
|
|
526
|
+
|
|
527
|
+
product_info = {
|
|
528
|
+
'inchi_key': inchi_key,
|
|
529
|
+
'smiles': Chem.MolToSmiles(frag_mol),
|
|
530
|
+
'formula': formula,
|
|
531
|
+
'molecular_weight': ExactMolWt(frag_mol),
|
|
532
|
+
'num_atoms': frag_mol.GetNumAtoms(),
|
|
533
|
+
'num_bonds': frag_mol.GetNumBonds(),
|
|
534
|
+
'fluorinated_atoms': len(frag_fluorinated_atoms),
|
|
535
|
+
'contains_fluorinated_chain': len(frag_fluorinated_atoms) > 0,
|
|
536
|
+
'n_breaks': n_breaks,
|
|
537
|
+
'broken_bonds': data['broken_bonds']
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
analysis['products'].append(product_info)
|
|
541
|
+
|
|
542
|
+
# Sort products by number of breaks and molecular weight
|
|
543
|
+
analysis['products'].sort(key=lambda x: (x['n_breaks'], x['molecular_weight']))
|
|
544
|
+
|
|
545
|
+
return analysis
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
analyze_degradation_pathways = analyse_degradation_pathways # US alias
|