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,256 @@
|
|
|
1
|
+
from .core import add_componentSmarts, get_substruct, remove_atoms
|
|
2
|
+
from .homologue_series import HomologueSeries
|
|
3
|
+
from rdkit import Chem
|
|
4
|
+
from rdkit.Chem.rdMolDescriptors import CalcMolFormula
|
|
5
|
+
import networkx as nx
|
|
6
|
+
from itertools import combinations
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _mol_from_input(mol_or_string):
|
|
10
|
+
"""Accept a Mol, SMILES string, or InChI string; always return a Chem.Mol.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
mol_or_string :
|
|
15
|
+
``rdkit.Chem.Mol``, a SMILES string, or an InChI string
|
|
16
|
+
(must start with ``'InChI='``).
|
|
17
|
+
|
|
18
|
+
Returns
|
|
19
|
+
-------
|
|
20
|
+
rdkit.Chem.Mol
|
|
21
|
+
|
|
22
|
+
Raises
|
|
23
|
+
------
|
|
24
|
+
ValueError
|
|
25
|
+
If the string cannot be parsed as a valid molecule.
|
|
26
|
+
TypeError
|
|
27
|
+
If the input type is not supported.
|
|
28
|
+
"""
|
|
29
|
+
if isinstance(mol_or_string, Chem.Mol):
|
|
30
|
+
return mol_or_string
|
|
31
|
+
if isinstance(mol_or_string, str):
|
|
32
|
+
s = mol_or_string.strip()
|
|
33
|
+
if s.startswith('InChI='):
|
|
34
|
+
mol = Chem.MolFromInchi(s)
|
|
35
|
+
else:
|
|
36
|
+
mol = Chem.MolFromSmiles(s)
|
|
37
|
+
if mol is None:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"Could not parse input as a molecule: {s!r}"
|
|
40
|
+
)
|
|
41
|
+
return mol
|
|
42
|
+
raise TypeError(
|
|
43
|
+
f"Expected rdkit.Chem.Mol or str, got {type(mol_or_string).__name__}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Halogen element symbols and their atomic numbers used to build per-halogen SMARTS
|
|
47
|
+
_HALOGEN_ATOMIC_NUM = {'F': 9, 'Cl': 17, 'Br': 35, 'I': 53}
|
|
48
|
+
|
|
49
|
+
# Default componentSmartsName per halogen (per-halo, alkyl chain)
|
|
50
|
+
_DEFAULT_COMPONENT_NAME = {
|
|
51
|
+
'F': 'Perfluoroalkyl',
|
|
52
|
+
'Cl': 'Perchloroalkyl',
|
|
53
|
+
'Br': 'Perbromoalkyl',
|
|
54
|
+
'I': 'Periodoalkyl',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def find_halogenated_components(mol, component_smarts, halogen='F'):
|
|
59
|
+
"""Find connected halogenated subgraphs in *mol* using a component SMARTS.
|
|
60
|
+
|
|
61
|
+
Unlike the old ``find_chain`` approach, this function does **not** require
|
|
62
|
+
``end`` SMARTS or shortest-path traversal. Instead it:
|
|
63
|
+
|
|
64
|
+
1. Matches every atom in *mol* against *component_smarts* (the ``component``
|
|
65
|
+
entry from ``component_smarts_halogens.json``).
|
|
66
|
+
2. Builds a subgraph restricted to the matched atoms.
|
|
67
|
+
3. Returns the connected components of that subgraph together with the set
|
|
68
|
+
of halogen-bearing carbon indices (candidate repeating-unit carbons)
|
|
69
|
+
within each component.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
mol : rdkit.Chem.Mol
|
|
74
|
+
Input molecule.
|
|
75
|
+
component_smarts : rdkit.Chem.Mol
|
|
76
|
+
Pre-compiled SMARTS mol object for the halogenated component pattern.
|
|
77
|
+
halogen : str, default ``'F'``
|
|
78
|
+
Halogen element symbol (``'F'``, ``'Cl'``, ``'Br'``, ``'I'``).
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
list of dict
|
|
83
|
+
Each entry describes one halogenated component:
|
|
84
|
+
|
|
85
|
+
- ``'component'`` (``frozenset[int]``) – atom indices of all component atoms.
|
|
86
|
+
- ``'cx2_carbons'`` (``list[int]``) – backbone C atoms bearing ≥ 2 halogen
|
|
87
|
+
substituents (i.e., candidate ``CX2`` units for homologue generation).
|
|
88
|
+
"""
|
|
89
|
+
component_atom_idxs = get_substruct(mol, component_smarts)
|
|
90
|
+
if not component_atom_idxs:
|
|
91
|
+
return []
|
|
92
|
+
|
|
93
|
+
# Build subgraph restricted to component atoms
|
|
94
|
+
G = nx.Graph()
|
|
95
|
+
G.add_nodes_from(component_atom_idxs)
|
|
96
|
+
for bond in mol.GetBonds():
|
|
97
|
+
a, b = bond.GetBeginAtomIdx(), bond.GetEndAtomIdx()
|
|
98
|
+
if a in component_atom_idxs and b in component_atom_idxs:
|
|
99
|
+
G.add_edge(a, b)
|
|
100
|
+
|
|
101
|
+
results = []
|
|
102
|
+
for cc in nx.connected_components(G):
|
|
103
|
+
# Identify CX2 backbone carbons: C in component bearing *exactly* 2 halogen
|
|
104
|
+
# neighbours. Terminal CF3 (3 halogens) and mono-halo atoms are excluded —
|
|
105
|
+
# only true -CX2- repeating units qualify.
|
|
106
|
+
cx2 = [
|
|
107
|
+
idx for idx in cc
|
|
108
|
+
if mol.GetAtomWithIdx(idx).GetSymbol() == 'C'
|
|
109
|
+
and sum(
|
|
110
|
+
1 for nb in mol.GetAtomWithIdx(idx).GetNeighbors()
|
|
111
|
+
if nb.GetSymbol() == halogen
|
|
112
|
+
) == 2
|
|
113
|
+
]
|
|
114
|
+
results.append({'component': frozenset(cc), 'cx2_carbons': cx2})
|
|
115
|
+
|
|
116
|
+
return results
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@add_componentSmarts()
|
|
120
|
+
def generate_homologues(mol_input, componentSmartsName=None, componentSmartss=None,
|
|
121
|
+
halogen='F', base_repeating=None):
|
|
122
|
+
"""Generate a homologous series by removing halogenated repeating units.
|
|
123
|
+
|
|
124
|
+
This implementation uses a **component-based** approach:
|
|
125
|
+
|
|
126
|
+
1. The ``component`` SMARTS from *componentSmartss* is used to identify all
|
|
127
|
+
atoms belonging to halogenated sub-structures (no ``end`` SMARTS needed).
|
|
128
|
+
2. Within each connected halogenated component, every carbon bearing ≥ 2
|
|
129
|
+
halogen atoms (a ``CX2`` unit) is a candidate for removal.
|
|
130
|
+
3. All non-empty subsets of those ``CX2`` units are tried; each subset is
|
|
131
|
+
removed via :func:`~HalogenGroups.core.remove_atoms`, rebuilding
|
|
132
|
+
connectivity across the gap.
|
|
133
|
+
4. Valid, connected products are collected and deduplicated by InChIKey.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
mol : rdkit.Chem.Mol
|
|
138
|
+
Parent molecule.
|
|
139
|
+
componentSmartsName : str, optional
|
|
140
|
+
Key in *componentSmartss* selecting which component pattern to use.
|
|
141
|
+
Defaults to the per-halogen value in ``_DEFAULT_COMPONENT_NAME``
|
|
142
|
+
(e.g. ``'Perfluoroalkyl'`` for ``halogen='F'``).
|
|
143
|
+
componentSmartss : dict, optional
|
|
144
|
+
Mapping ``{name: {'component': <Mol>, ...}}``.
|
|
145
|
+
Populated automatically by the ``@add_componentSmarts`` decorator.
|
|
146
|
+
halogen : str, default ``'F'``
|
|
147
|
+
Halogen element symbol. Controls which atoms count as the repeating
|
|
148
|
+
substituents and which component pattern is selected by default.
|
|
149
|
+
Accepted values: ``'F'``, ``'Cl'``, ``'Br'``, ``'I'``.
|
|
150
|
+
base_repeating : list of str, optional
|
|
151
|
+
Backbone element symbols that are *kept* when a CX2 unit is removed
|
|
152
|
+
(their halogen substituents are stripped, not the backbone C itself).
|
|
153
|
+
Defaults to ``['C']``.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
dict
|
|
158
|
+
``{InChIKey: {formula: rdkit.Chem.Mol}}`` – all unique shorter
|
|
159
|
+
homologues, keyed first by InChIKey and then by molecular formula.
|
|
160
|
+
|
|
161
|
+
Raises
|
|
162
|
+
------
|
|
163
|
+
ValueError
|
|
164
|
+
If *halogen* is not one of ``'F'``, ``'Cl'``, ``'Br'``, ``'I'``.
|
|
165
|
+
ValueError
|
|
166
|
+
If atom removal yields a fragmented molecule (diagnostic aid).
|
|
167
|
+
|
|
168
|
+
Examples
|
|
169
|
+
--------
|
|
170
|
+
>>> from rdkit import Chem
|
|
171
|
+
>>> from HalogenGroups.generate_homologues import generate_homologues
|
|
172
|
+
>>> # PFOA – generate all shorter perfluoroalkyl chain homologues
|
|
173
|
+
>>> pfoa = Chem.MolFromSmiles('OC(=O)' + 'C(F)(F)' * 7 + 'F')
|
|
174
|
+
>>> homologues = generate_homologues(pfoa)
|
|
175
|
+
>>> print(len(homologues)) # 6 (C2-C7)
|
|
176
|
+
|
|
177
|
+
>>> # Chlorinated analogue
|
|
178
|
+
>>> pca = Chem.MolFromSmiles('OC(=O)' + 'C(Cl)(Cl)' * 5 + 'Cl')
|
|
179
|
+
>>> homologues = generate_homologues(pca, halogen='Cl')
|
|
180
|
+
|
|
181
|
+
>>> # From SMILES string directly
|
|
182
|
+
>>> homologues = generate_homologues('OC(=O)C(F)(F)C(F)(F)C(F)(F)F')
|
|
183
|
+
|
|
184
|
+
>>> # From InChI string
|
|
185
|
+
>>> homologues = generate_homologues('InChI=1S/C3HF5O2/...')
|
|
186
|
+
|
|
187
|
+
Notes
|
|
188
|
+
-----
|
|
189
|
+
* Removing a CX2 unit strips the backbone carbon **and** all directly
|
|
190
|
+
attached halogen atoms; connectivity to the rest of the molecule is
|
|
191
|
+
preserved by :func:`~HalogenGroups.core.remove_atoms`.
|
|
192
|
+
* Branched structures are handled correctly because component detection
|
|
193
|
+
is graph-based, not path-based.
|
|
194
|
+
* The original molecule is *not* included in the output.
|
|
195
|
+
"""
|
|
196
|
+
# Accept SMILES / InChI strings in addition to Chem.Mol
|
|
197
|
+
mol = _mol_from_input(mol_input)
|
|
198
|
+
|
|
199
|
+
if halogen not in _HALOGEN_ATOMIC_NUM:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"halogen must be one of {list(_HALOGEN_ATOMIC_NUM)}, got {halogen!r}"
|
|
202
|
+
)
|
|
203
|
+
if base_repeating is None:
|
|
204
|
+
base_repeating = ['C']
|
|
205
|
+
|
|
206
|
+
# Resolve component SMARTS name
|
|
207
|
+
if componentSmartsName is None:
|
|
208
|
+
componentSmartsName = _DEFAULT_COMPONENT_NAME[halogen]
|
|
209
|
+
|
|
210
|
+
# Look up the component SMARTS mol object
|
|
211
|
+
entry = componentSmartss.get(componentSmartsName)
|
|
212
|
+
if entry is None:
|
|
213
|
+
raise ValueError(
|
|
214
|
+
f"Component '{componentSmartsName}' not found in componentSmartss. "
|
|
215
|
+
f"Available: {list(componentSmartss)}"
|
|
216
|
+
)
|
|
217
|
+
component_smarts = entry['component'] if isinstance(entry, dict) else entry
|
|
218
|
+
|
|
219
|
+
# Atoms that are removed together with a CX2 backbone carbon
|
|
220
|
+
# (only the halogen neighbours are stripped alongside the backbone C)
|
|
221
|
+
|
|
222
|
+
# Discover halogenated components and their CX2 carbons
|
|
223
|
+
components = find_halogenated_components(mol, component_smarts, halogen=halogen)
|
|
224
|
+
|
|
225
|
+
# Collect all CX2 carbon indices across all components
|
|
226
|
+
all_cx2 = [idx for comp in components for idx in comp['cx2_carbons']]
|
|
227
|
+
|
|
228
|
+
series = HomologueSeries()
|
|
229
|
+
|
|
230
|
+
if not all_cx2:
|
|
231
|
+
series._set_metadata(mol, halogen, componentSmartsName)
|
|
232
|
+
return series
|
|
233
|
+
|
|
234
|
+
# Enumerate every non-empty proper subset of CX2 units
|
|
235
|
+
for r in range(1, len(all_cx2) + 1):
|
|
236
|
+
for subset in combinations(all_cx2, r):
|
|
237
|
+
flat_idx = list(subset)
|
|
238
|
+
try:
|
|
239
|
+
h = remove_atoms(mol, flat_idx, removable=[halogen])
|
|
240
|
+
except Exception as e:
|
|
241
|
+
print(
|
|
242
|
+
f"Error removing atoms {flat_idx} from "
|
|
243
|
+
f"{Chem.MolToSmiles(mol)}: {e}"
|
|
244
|
+
)
|
|
245
|
+
raise e
|
|
246
|
+
if len(Chem.GetMolFrags(h)) > 1:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
f"Fragmented molecule {Chem.MolToSmiles(h)} after removing "
|
|
249
|
+
f"atoms {flat_idx} from {Chem.MolToSmiles(mol)}"
|
|
250
|
+
)
|
|
251
|
+
inchikey = Chem.MolToInchiKey(h)
|
|
252
|
+
formula = CalcMolFormula(h)
|
|
253
|
+
series.setdefault(inchikey, {})[formula] = h
|
|
254
|
+
|
|
255
|
+
series._set_metadata(mol, halogen, componentSmartsName)
|
|
256
|
+
return series
|