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.
@@ -0,0 +1,724 @@
1
+ Metadata-Version: 2.4
2
+ Name: PFASGroups
3
+ Version: 3.2.2
4
+ Summary: A comprehensive cheminformatics package for automated detection, classification, and analysis of halogenated substances, with a focus on per- and polyfluoroalkyl substances (PFAS). Combines SMARTS pattern matching, molecular formula constraints, and graph-based pathfinding to identify 119 groups. Supports creations of embeddings with graph metrics for Machine Learning workflows.
5
+ Author: Luc T. Miaz
6
+ Author-email: "Luc T. Miaz" <luc@miaz.ch>
7
+ Maintainer-email: "Luc T. Miaz" <luc@miaz.ch>
8
+ License-Expression: CC-BY-NC-4.0
9
+ Project-URL: Homepage, https://github.com/lucmiaz/PFASGroups
10
+ Project-URL: Repository, https://github.com/lucmiaz/PFASGroups
11
+ Project-URL: Documentation, https://pfasgroups.readthedocs.io
12
+ Project-URL: Bug Tracker, https://github.com/lucmiaz/PFASGroups/issues
13
+ Keywords: PFAS,halogenated substances,per- and polyfluoroalkyl substances,chemical classification,cheminformatics,environmental chemistry,molecular structure,fingerprint,multi-halogen,fluorine,chlorine,bromine,iodine
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.7
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
25
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
26
+ Requires-Python: >=3.7
27
+ Description-Content-Type: text/markdown
28
+ Requires-Dist: rdkit
29
+ Requires-Dist: numpy
30
+ Requires-Dist: pandas
31
+ Requires-Dist: networkx
32
+ Requires-Dist: tqdm
33
+ Requires-Dist: svgutils
34
+ Requires-Dist: scikit-learn
35
+ Requires-Dist: matplotlib
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest; extra == "dev"
38
+ Requires-Dist: pytest-cov; extra == "dev"
39
+ Provides-Extra: database
40
+ Requires-Dist: sqlalchemy>=1.4.0; extra == "database"
41
+
42
+ # PFASGroups
43
+
44
+ A comprehensive cheminformatics package for automated detection, classification, and analysis of halogenated substances, in particular per- and polyfluoroalkyl substances (PFAS) in chemical databases.
45
+
46
+ ## Overview
47
+
48
+ PFASGroups combines SMARTS pattern matching, molecular formula constraints, and graph-based pathfinding (using RDKit and NetworkX) to identify and classify PFAS compounds. The package enables systematic PFAS universe mapping and environmental monitoring applications.
49
+
50
+ ## Key Features
51
+
52
+ ### Core Capabilities
53
+ - **Halogen Group Identification**: Automated detection of 119 functional groups (114 compiled for fluorine-only embedding):
54
+ - 27 PFAS OECD groups
55
+ - 48 generic functional groups (IDs 29–73, 117–119; the last 3 are halogen-context-dependent or recently added)
56
+ - 43 fluorotelomer-specific groups with CH₂ linker validation
57
+ - 1 aggregate pattern-matching group (Group 116: Telomers, `compute=False`)
58
+ - **Atom Reference Requirement**: For non-telomer groups, SMARTS patterns must match atoms that are part of or directly connected to the fluorinated component (per/polyfluorinated carbons), respecting the `max_dist_from_comp` constraint
59
+ - **Linker Validation**: CH₂-specific validation for 40 fluorotelomer groups to distinguish from direct-attachment analogues. Telomer groups use `linker_smarts` to allow functional groups separated from perfluoro chains by non-fluorinated linkers
60
+ - **Aggregate Groups**: Pattern-matching groups that collect related PFAS groups via regex (e.g., Group 113 matches all "telomer" groups)
61
+ - **Component Length Analysis**: Quantification of per- and polyfluorinated alkyl components with CF₂ unit counting
62
+ - **Graph Metrics**: Comprehensive structural characterization (branching, eccentricity, diameter, resistance, centrality, telomer spacer length, ring size)
63
+ - **Customizable Definitions**: Easy extension to additional PFAS groups and halogenated chemical classes via JSON configuration. Component type definitions in `data/component_smarts_halogens.json` support optional per-type `constraints` (e.g. `{"gte": {"F": 2}}`) that are evaluated against the component's full atom count (backbone carbons plus attached halogens) to enforce minimum halogen counts or element exclusions — without repeating the check in every group definition
64
+
65
+ ### Additional Tools
66
+ - **Homologue Series Generation**: Iterative component shortening to explore theoretical chemical space
67
+ - **Fingerprint Generation**: PFAS fingerprints for machine learning applications
68
+ - **Visualization**: Assign and visualize PFAS groupings
69
+ - **Multiple Interfaces**: Python API, command-line tool, and browser-based JavaScript version (RDKitJS)
70
+ - **Batch Processing**: Efficient analysis of large chemical databases
71
+
72
+ ## Installation
73
+
74
+ ### From Pypi
75
+
76
+ ```sh
77
+ pip install PFASGroups
78
+ ```
79
+
80
+ ### From source (recommended for development)
81
+
82
+ **Prerequisites**: Python ≥ 3.7, RDKit (install via conda or pip before the steps below).
83
+
84
+ ```sh
85
+ # Clone the repository
86
+ git clone https://github.com/lucmiaz/PFASGroups.git
87
+ cd PFASGroups
88
+
89
+ # Install in editable mode (development install)
90
+ pip install -e .
91
+ ```
92
+
93
+ > **Note for conda users**: install RDKit via conda before running pip:
94
+ > ```sh
95
+ > conda install -c conda-forge rdkit
96
+ > ```
97
+
98
+ After installation, both `HalogenGroups` (all halogens by default) and `PFASgroups` (fluorine only by default) are importable, and the `pfasgroups` CLI command is available in your terminal.
99
+
100
+ ### Verify installation
101
+
102
+ ```python
103
+ from PFASGroups import parse_smiles
104
+ results = parse_smiles("FC(F)(F)C(F)(F)C(=O)O") # → PFASEmbeddingSet
105
+ print(results) # prints PFASEmbeddingSet summary (molecule count, matched groups, …)
106
+ print(results[0]) # prints PFASEmbedding summary for the first molecule
107
+ ```
108
+
109
+ ## Repository Structure
110
+
111
+ ```
112
+ PFASGroups/
113
+ ├── HalogenGroups/ # Multi-halogen wrapper package
114
+ │ └── __init__.py # Wraps PFASgroups; defaults halogens=['F','Cl','Br','I']
115
+ ├── PFASGroups/ # Core implementation package (also importable as PFASgroups)
116
+ │ ├── __init__.py # Public API
117
+ │ ├── core.py # SMARTS matching engine, component detection, decorators
118
+ │ ├── parser.py # parse_smiles / parse_mols entry points
119
+ │ ├── embeddings.py # presets
120
+ │ ├── PFASEmbeddings.py # PFASEmbeddingSet, PFASEmbedding
121
+ │ ├── HalogenGroupModel.py # HalogenGroup data model
122
+ │ ├── PFASDefinitionModel.py # PFASDefinition model
123
+ │ ├── ComponentsSolverModel.py # Graph-based path-finding solver
124
+ │ ├── getter.py # get_HalogenGroups, get_componentSMARTSs, …
125
+ │ ├── cli.py # Command-line interface (halogengroups / pfasgroups)
126
+ │ ├── prioritise.py # prioritise_molecules
127
+ │ ├── generate_homologues.py # Homologue series generation
128
+ │ ├── fragmentation.py # Fragment utilities
129
+ │ ├── draw_mols.py # Molecule and group visualisation helpers
130
+ │ └── data/ # JSON configuration files (bundled with the package)
131
+ │ ├── Halogen_groups_smarts.json # 113 halogen group definitions
132
+ │ ├── component_smarts.json # Fluorinated component SMARTS patterns
133
+ │ ├── component_smarts_halogens.json # Multi-halogen component SMARTS patterns
134
+ │ └── PFAS_definitions_smarts.json # PFAS regulatory definitions
135
+ ├── tests/ # Pytest test suite (25+ test modules)
136
+ │ ├── test_halogen_groups_smarts.py
137
+ │ ├── test_results_fingerprint.py
138
+ │ ├── test_results_model.py
139
+ │ ├── test_prioritise.py
140
+ │ └── …
141
+ ├── examples/ # Standalone usage example scripts
142
+ │ ├── results_fingerprint_analysis.py
143
+ │ ├── prioritization_examples.py
144
+ │ └── …
145
+ ├── docs/ # Sphinx documentation source
146
+ │ ├── quickstart.rst
147
+ │ ├── algorithm.rst
148
+ │ ├── customization.rst
149
+ │ ├── ResultsFingerprint_Guide.md
150
+ │ └── …
151
+ ├── benchmark/ # Benchmarking scripts and timing reports
152
+ │ ├── data/
153
+ │ ├── reports/
154
+ │ └── …
155
+ └── pyproject.toml # Package metadata and build configuration
156
+ ```
157
+
158
+ **Two importable packages, one codebase:**
159
+
160
+ | Package | Default `halogens` | Typical use |
161
+ |---|---|---|
162
+ | `HalogenGroups` | `['F', 'Cl', 'Br', 'I']` (all) | Multi-halogen analysis |
163
+ | `PFASgroups` | `'F'` (fluorine only) | PFAS / fluorine-focused analysis |
164
+
165
+ ## Benchmark Summary (Mar 2026, v. 3.2.2, F groups)
166
+
167
+ Benchmarks were run on an Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz (4C/8T), 15.5 GB RAM, Python 3.9.23, RDKit 2025.09.2, NetworkX 3.2.1 (the old version of Python was taken for compatibility with PFAS-atlas).
168
+
169
+ | Dataset/Profile | Count | Atom range | PFASgroups mean/median (ms) | PFAS-Atlas mean/median (ms) | Relative speed | Notes |
170
+ | --- | --- | --- | --- | --- | --- | --- |
171
+ | OECD reference (real compounds) | 3,707 | varied | 26.3 / 20.8 | 32.4 / 37.0 | 1.23x faster | Real-world OECD dataset. |
172
+ | CLInventory, F only (real compounds) | 28,328 | varied | 23.2 / 17.5 | 5.1 / 1.0 | 4.6x slower | Large real-world dataset; Atlas faster due to smaller typical molecule size. |
173
+ | Large PFAS (≥35 atoms) | 1,242 | ≥35 | 526.0 / 253.4 | 93.8 / 72.5 | 5.6x slower | Large-molecule subset; heavy-tail runtime. |
174
+ | Stress-test (full metrics) | 2,500 | 11-622 | 271.8 / 35.9 | 67.0 / 38.7 | 4.1x slower | Synthetic stress-test; heavy-tail runtime. |
175
+ | Stress-test (no EGR) | 2,500 | 11-625 | 272.6 / 31.6 | 67.2 / 38.7 | ≈ full | Disables effective graph resistance; negligible speedup. |
176
+ | Stress-test (no metrics) | 2,500 | 9-621 | 205.1 / 28.7 | 67.0 / 38.4 | 1.33x faster vs full | Disables all component graph metrics. |
177
+
178
+ Timing profile plots (full vs no EGR vs no metrics):
179
+ - [benchmark/imgs/si/timing_profiles_comparison.png](benchmark/imgs/si/timing_profiles_comparison.png)
180
+
181
+ Disable or limit graph metrics in the Python API:
182
+
183
+ ```python
184
+ from PFASGroups import parse_smiles
185
+
186
+ smiles_list = ['C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O',
187
+ 'C1=CC(=CC=C1[N+](=O)[O-])OC2=C(C=C(C=C2)C(F)(F)F)[N+](=O)[O-]',
188
+ 'C(C(C(C(F)(F)S(=O)(=O)O)(F)F)(F)F)(C(C(F)(F)F)(F)F)(C(F)(F)F)C(F)(F)F'
189
+ ]
190
+
191
+ # Skip all component graph metrics (fastest)
192
+ rFull = parse_smiles(smiles_list, compute_component_metrics=False)
193
+ print(rFull)
194
+ rFull.summary()
195
+
196
+ # Keep metrics but skip effective graph resistance entirely
197
+ rNoEGR = parse_smiles(smiles_list, limit_effective_graph_resistance=0)
198
+ print(rNoEGR)
199
+
200
+ # Compute resistance only for components below a size threshold
201
+ rLimitEGR = parse_smiles(smiles_list, limit_effective_graph_resistance=20)
202
+ print(rLimitEGR)
203
+ ```
204
+
205
+ CLI equivalents:
206
+
207
+ ```bash
208
+ # Skip all component graph metrics (fastest)
209
+ pfasgroups parse --no-component-metrics "C(C(F)(F)F)F"
210
+
211
+ # Skip effective graph resistance entirely
212
+ pfasgroups parse --limit-effective-graph-resistance 0 "C(C(F)(F)F)F"
213
+
214
+ # Compute resistance only for components below a size threshold
215
+ pfasgroups parse --limit-effective-graph-resistance 200 "C(C(F)(F)F)F"
216
+ ```
217
+
218
+ ## Quick Start
219
+
220
+ ### Python API
221
+
222
+ ```python
223
+ from PFASGroups import parse_smiles
224
+
225
+ # Parse PFAS structures — returns a PFASEmbeddingSet
226
+ smiles_list = ["C[Si](C)(Cl)CCC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
227
+ "FC(=C(C(F)(F)F)C(F)(F)F)C(F)(F)C(F)(F)F",
228
+ "FC(F)(F)C(F)(C(F)(F)F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(C(F)(F)F)C(F)(F)F",
229
+ "C[Si](Cl)(Cl)CCC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
230
+ "FC(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
231
+ "CC(=O)C(F)(F)C(F)(F)C(F)(F)F",
232
+ "FC(F)=C(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
233
+ "FC(F)(F)N1C(F)(F)C(F)(F)C(F)(F)C(F)(F)C1(F)F",
234
+ "FC(F)(F)C(F)(F)C(F)(F)CCl",
235
+ "OCC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
236
+ "OS(=O)(=O)CCC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
237
+ "FC(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)COC(=O)C=C",
238
+ "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)OC=C",
239
+ "FC(F)(F)C(I)(C(F)(F)F)C(F)(F)F",
240
+ "OC(C(F)(F)C(F)(F)F)(C(F)(F)C(F)(F)F)C(F)(F)C(F)(F)F",
241
+ "C[Si](Cl)(Cl)CCC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
242
+ "FC(F)=C(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
243
+ "CCN(CC)CC.NS(=O)(=O)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
244
+ "[OH-].C[N+](C)(C)CCCNS(=O)(=O)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
245
+ "[K+].[O-]S(=O)(=O)C(F)(F)C(F)(C(F)(F)F)C(F)(F)F",
246
+ "[NH4+].[O-]P(=O)(OCCC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F)OCCC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
247
+ "OCCN(CCO)S(=O)(=O)C(F)(F)C(F)(F)C(F)(F)C(F)F",
248
+ "OP(O)(=O)OCCC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(C(F)(F)F)C(F)(F)F",
249
+ "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)N(C(F)(F)C(F)(F)C(F)(F)C(F)(F)F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
250
+ "COC(=O)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
251
+ "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)OCC=C",
252
+ "CC(C)CCOC(=O)CC(NC(=O)C(F)(F)C(F)(F)C(F)(F)F)C(=O)OCCC(C)C",
253
+ "CN(C(=O)C(F)(F)C(F)(F)C(F)(F)F)C(=O)C(F)(F)C(F)(F)C(F)(F)F",
254
+ "FC(F)(F)C(F)(F)C(F)(F)C=CI",
255
+ "OC(=O)C1=CC=CC=C1NC(=O)C(F)(F)C(F)(F)C(F)(F)F",
256
+ "FC(F)(F)C1(F)C(F)(F)C(F)(F)C(F)(F)C(F)(C(F)(F)F)C1(F)F"]
257
+
258
+ results = parse_smiles(smiles_list) # → PFASEmbeddingSet
259
+ print(results) # summary: molecule count, top groups, …
260
+ print(results[0]) # per-molecule summary for first entry
261
+
262
+ # Embedding from a pre-parsed set (avoids re-parsing)
263
+ arr = results.to_array() # default: binary, all groups
264
+ arr = results.to_array(group_selection='oecd', component_metrics=['binary'])
265
+ cols = results.column_names() # matching column labels
266
+
267
+ # Filter components by halogen, form, and saturation
268
+ results_f = parse_smiles(smiles_list, halogens='F') # Fluorine only
269
+ results_pfa = parse_smiles(smiles_list, halogens='F', saturation='per', form='alkyl') # Perfluoroalkyl only
270
+ results_cyclic = parse_smiles(smiles_list, form='cyclic') # Cyclic forms only
271
+
272
+ # Dimensionality reduction (methods on PFASEmbeddingSet)
273
+ pca_result = results.perform_pca(n_components=5, plot=True)
274
+ tsne_result = results.perform_tsne(perplexity=30, plot=True)
275
+ umap_result = results.perform_umap(n_neighbors=15, plot=True)
276
+
277
+ # Colour each point by the PFAS group with the highest match count
278
+ pca_result = results.perform_pca(n_components=5, color_by='top_group')
279
+ tsne_result = results.perform_tsne(perplexity=30, color_by='top_group')
280
+ umap_result = results.perform_umap(n_neighbors=15, color_by='top_group')
281
+
282
+ # Or supply your own per-molecule labels (e.g. from external metadata)
283
+ labels = ['industrial'] * 10 + ['environmental'] * (len(smiles_list) - 10)
284
+ pca_result = results.perform_pca(color_by=labels)
285
+
286
+ # Compare two datasets using KL divergence
287
+ other_smiles_list = smiles_list[0:10] + \
288
+ ["OC(=O)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(O)=O",
289
+ "OCC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F",
290
+ "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)COCC=C"]
291
+ other_results = parse_smiles(other_smiles_list)
292
+ similarity = results.compare_kld(other_results, method='minmax')
293
+ print(f"Similarity between the lists of compounds: {similarity:2.3f}")
294
+
295
+ # Save to SQL database
296
+ results.to_sql(filename='results.db')
297
+
298
+ # Prioritization tool for screening and ranking
299
+ from PFASGroups import prioritise_molecules
300
+
301
+ # Prioritize by similarity to reference list (e.g., known persistent PFAS)
302
+ reference = ["FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O"]
303
+ prioritised, scores = prioritise_molecules(
304
+ molecules=smiles_list,
305
+ reference=reference,
306
+ return_scores=True
307
+ )
308
+ print(f"Prioritisation order: {'\n'.join([x['smiles'] for x in prioritised])}")
309
+
310
+ # Prioritize by intrinsic fluorination properties (long chains, high F content)
311
+ prioritised = prioritise_molecules(
312
+ molecules=smiles_list,
313
+ a=1.0, # Weight for total fluorination
314
+ b=5.0, # Weight for longest chains
315
+ percentile=90, # Focus on 90th percentile of component sizes
316
+ return_scores = False
317
+ )
318
+ print(f"Prioritisation order: {'\n'.join([x['smiles'] for x in prioritised])}")
319
+
320
+ ```
321
+
322
+ ### Command Line
323
+
324
+ ```bash
325
+ # Parse SMILES strings
326
+ pfasgroups parse "C(C(F)(F)F)F" "FC(F)(F)C(F)(F)C(=O)O"
327
+
328
+ # Generate fingerprints
329
+ pfasgroups fingerprint "C(C(F)(F)F)F" --format dict
330
+
331
+ # List available PFAS groups
332
+ pfasgroups list-groups
333
+ ```
334
+
335
+ ### Filtering Components by Halogen, Form, and Saturation
336
+
337
+ Filter component matches by specific halogens, chemical forms, or saturation levels:
338
+
339
+ ```python
340
+ from HalogenGroups import parse_smiles
341
+
342
+ smiles_list = ["C(C(F)(F)F)F", "FC(F)(F)C(F)(F)C(=O)O", "C(C(Cl)(Cl)Cl)Cl"]
343
+
344
+ # Filter only fluorine components
345
+ results_f = parse_smiles(smiles_list, halogens='F')
346
+
347
+ # Filter perfluorinated alkyl compounds
348
+ results_pfa = parse_smiles(smiles_list, halogens='F', saturation='per', form='alkyl')
349
+
350
+ # Filter polyfluorinated cyclic compounds
351
+ results_polyf_cyclic = parse_smiles(smiles_list, halogens='F', saturation='poly', form='cyclic')
352
+
353
+ # Filter multiple halogens (F and Cl)
354
+ results_multi = parse_smiles(smiles_list, halogens=['F', 'Cl'])
355
+
356
+ # Valid filter options:
357
+ # - halogens: 'F', 'Cl', 'Br', 'I', or list like ['F', 'Cl']
358
+ # - saturation: 'per' or 'poly' (or list like ['per', 'poly'] for both)
359
+ # - form: 'alkyl' or 'cyclic' (or list like ['alkyl', 'cyclic'] for both)
360
+ ```
361
+
362
+ ## Embedding with Graph Metrics
363
+
364
+ The `to_array()` / `to_fingerprint()` methods accept a `component_metrics` list that
365
+ stacks one block of *N_G* columns per metric (default `N_G = 114` for fluorine-only).
366
+
367
+ ```python
368
+ import numpy as np
369
+ from PFASGroups import parse_smiles, EMBEDDING_PRESETS
370
+
371
+ smiles = [
372
+ "O=C(O)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F", # PFOA
373
+ "O=C(O)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F", # PFHpA
374
+ "OCC(F)(F)C(F)(F)C(F)(F)C(F)(F)F", # 4:2 FTOH
375
+ "OCC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)F", # 6:2 FTOH
376
+ ]
377
+ results = parse_smiles(smiles)
378
+
379
+ # --- binary embedding (default) ---
380
+ arr_bin = results.to_array() # shape (4, 114)
381
+
382
+ # --- preset 'best': binary + effective_graph_resistance ---
383
+ # Best discrimination (mean Tanimoto 0.184, outperforms TxP-PFAS 129-bit)
384
+ print(EMBEDDING_PRESETS['best']['description'])
385
+ arr_best = results.to_array(preset='best') # shape (4, 228) = 114 × 2
386
+
387
+ print(arr_best.shape)
388
+
389
+ # --- effective graph resistance directly ---
390
+ arr_egr = results.to_array(
391
+ component_metrics=['binary', 'effective_graph_resistance']
392
+ ) # shape (4, 228)
393
+
394
+ print(arr_egr.shape)
395
+
396
+ # --- n_spacer: telomer CH₂ spacer length (encodes 'm' in 'm:n' notation) ---
397
+ # Zero for non-telomers; distinguishes 2:1, 4:2, 6:2 FTOHs in a single column
398
+ arr_ns = results.to_array(component_metrics=['n_spacer']) # shape (4, 114)
399
+
400
+ print(arr_egr.ns)
401
+
402
+ # --- ring_size: smallest ring overlapping each matched component ---
403
+ # Zero for acyclic groups; 5 for azoles/furans; 6 for benzene/cyclohexane
404
+ arr_rs = results.to_array(component_metrics=['ring_size']) # shape (4, 114)
405
+
406
+ print(arr_rs.shape)
407
+
408
+ # --- combined: EGR + n_spacer + ring_size + molecule-wide descriptors ---
409
+ arr_combined = results.to_array(
410
+ component_metrics=['binary', 'effective_graph_resistance',
411
+ 'n_spacer', 'ring_size'],
412
+ molecule_metrics=['n_components', 'max_size',
413
+ 'mean_branching', 'max_branching',
414
+ 'mean_component_fraction', 'max_component_fraction'],
415
+ ) # shape (4, 4*114 + 6) = (4, 462)
416
+
417
+ print(arr_combined.shape)
418
+
419
+ # --- multi-halogen: one parse per halogen, then hstack ---
420
+ halogens = ['F','Cl','Br','I']
421
+ r_hal = parse_smiles(smiles, halogens=halogens)
422
+ arr_4x = r_hal.to_array(component_metrics=['effective_graph_resistance'], molecule_metrics=['n_components','max_size','mean_branching'], halogens=halogens)
423
+
424
+ print(arr_4x.shape)
425
+
426
+ # --- column names ---
427
+ cols = results.column_names(
428
+ component_metrics=['binary', 'effective_graph_resistance']
429
+ )
430
+ print(cols[:4]) # e.g. ['Perfluoroalkyl [binary]', ..., 'Perfluoroalkyl [EGR]', ...]
431
+
432
+ print(arr_4x[:4])
433
+ ```
434
+
435
+ See [`examples/embedding_with_graph_metrics.py`](examples/embedding_with_graph_metrics.py)
436
+ for a complete runnable script covering all options.
437
+
438
+
439
+ ## Multi-Halogen Analysis
440
+
441
+ PFASGroups supports fluorine, chlorine, bromine, and iodine. There are two ways
442
+ to analyse all halogens at once:
443
+
444
+ ### Option A – import `HalogenGroups` (all halogens by default)
445
+
446
+ ```python
447
+ from HalogenGroups import parse_smiles
448
+
449
+ smiles_list = [
450
+ "C(C(F)(F)F)F", # fluorinated
451
+ "ClC(Cl)(Cl)C(Cl)(Cl)Cl", # chlorinated
452
+ "BrC(Br)(Br)CBr", # brominated
453
+ ]
454
+
455
+ # halogens defaults to ['F','Cl','Br','I'] — no extra argument needed
456
+ results = parse_smiles(smiles_list) # → PFASEmbeddingSet
457
+
458
+ # to_array() reads the halogen info already captured during parsing
459
+ arr = results.to_array(group_selection='oecd', component_metrics=['binary'], halogens = ['F','Cl','Br','I'])
460
+ cols = results.column_names(group_selection='oecd')
461
+
462
+ print(arr.shape)
463
+ ```
464
+
465
+ ### Option B – import `PFASgroups` and specify `halogens` explicitly
466
+
467
+ ```python
468
+ from PFASgroups import parse_smiles
469
+
470
+ smiles_list = [
471
+ "C(C(F)(F)F)F",
472
+ "ClC(Cl)(Cl)C(Cl)(Cl)Cl",
473
+ "BrC(Br)(Br)CBr",
474
+ ]
475
+
476
+ # Explicitly pass all halogens
477
+ results = parse_smiles(smiles_list, halogens=['F', 'Cl', 'Br', 'I']) # → PFASEmbeddingSet
478
+
479
+ arr = results.to_array(group_selection='oecd', component_metrics=['binary'], halogens=['F', 'Cl', 'Br', 'I'])
480
+ cols = results.column_names(group_selection='oecd')
481
+ ```
482
+
483
+ ## Custom Configuration
484
+
485
+ Use custom pathtype definitions and PFAS groups:
486
+
487
+ ```python
488
+ # Load custom files entirely
489
+ from PFASGroups import get_componentSMARTSs, get_HalogenGroups, parse_smiles
490
+ from PFASGroups.core import preprocess_componentsSmarts
491
+ from PFASGroups.parser import preprocess_HalogenGroups
492
+
493
+ # You can load component smarts
494
+ custom_paths = get_componentSMARTSs()
495
+ # Add or modify componentSMARTSs as you want
496
+ # ....
497
+ # then preprocess them
498
+ custom_paths = preprocess_componentsSmarts(custom_paths)
499
+
500
+
501
+ # You can load HalogenGroups
502
+ custom_groups = get_HalogenGroups()
503
+ # Add or modify HalogenGroups as you want
504
+ # ....
505
+ # then preprocess them
506
+ pfas_groups, agg_groups = preprocess_HalogenGroups(custom_groups)
507
+
508
+
509
+ results = parse_smiles(
510
+ ["C(C(F)(F)F)F"],
511
+ componentSmartss=custom_paths,
512
+ pfas_groups=pfas_groups,
513
+ agg_pfas_groups = agg_groups
514
+ )
515
+ ```
516
+
517
+ ```python
518
+ # Or extend defaults with your custom groups
519
+ from PFASGroups import get_compiled_HalogenGroups, HalogenGroup, parse_smiles, get_componentSMARTSs
520
+ from PFASGroups.core import preprocess_componentsSmarts
521
+
522
+ # Add custom PFAS groups
523
+ groups = get_compiled_HalogenGroups() # Get defaults, precompiled
524
+ # Create a new group
525
+ custom_group = HalogenGroup(
526
+ id=999,
527
+ name="My Custom Group",
528
+ smarts={"[C](F)(F)F": 1},
529
+ componentSmarts=None,
530
+ componentSaturation="per",
531
+ componentHalogens="F",
532
+ componentForm="alkyl",
533
+ constraints={"gte": {"F": 1}}
534
+ )
535
+
536
+ # then add it to the lit of groups
537
+ groups.append(custom_group)
538
+
539
+ # pass the new set of groups as argument to parse_smiles
540
+ results = parse_smiles(["FC(F)(F)C(F)(F)[N+](=O)[O-]"], pfas_groups=groups)
541
+
542
+ # Alternatively, select and modify some existing default_groups
543
+ from PFASGroups import get_HalogenGroups, HalogenGroup
544
+ # get and subset the default groups
545
+ default_groups = get_HalogenGroups()[2:4]
546
+ # extract one group
547
+ modified_group = default_groups[1]
548
+ # modify its parameters, here simply require presence of at least two structures
549
+ modified_group['smarts']={x:2 for x, i in modified_group['smarts'].items()}
550
+ # replace the modified group in the groups list
551
+ default_groups[1] = modified_group
552
+ # compile the groups
553
+ compiled_groups = [HalogenGroup(**x) for x in default_groups]
554
+
555
+ # Possibly add another group with max_dist_from_comp = 3
556
+ compiled_groups.append(HalogenGroup(
557
+ id=998,
558
+ name="Extended Distance Group",
559
+ smarts={"[#6$([#6][OH1])]": 1},
560
+ componentSmarts=None,
561
+ constraints={},
562
+ max_dist_from_comp=3 # Allow up to 3 bonds from fluorinated carbon
563
+ ))
564
+
565
+ # parse the groups:
566
+ results = parse_smiles(["FC(F)(F)C(F)(F)[N+](=O)[O-]"], pfas_groups=compiled_groups)
567
+
568
+ # Add or modify custom path types (e.g., chlorinated analogs)
569
+ paths = get_componentSMARTSs()
570
+ paths['Cl']['alkyl'] = {"per":{
571
+ "component":"[C;X4](Cl)(Cl)!@!=!#[C;X4](Cl)(Cl)",
572
+ "name":"Custom Chloro"}}
573
+ paths = preprocess_componentsSmarts(paths)
574
+
575
+ results = parse_smiles(["ClC(Cl)(Cl)C(Cl)(Cl)C(=O)O"], halogens = 'Cl', componentSmartss=paths)
576
+ ```
577
+
578
+ ## Documentation
579
+
580
+ - **[USER_GUIDE.md](USER_GUIDE.md)** - Complete documentation with examples
581
+ - **[QUICK_REFERENCE.md](QUICK_REFERENCE.md)** - Quick reference for common tasks
582
+
583
+ ## Usage Examples
584
+
585
+ See [USER_GUIDE.md](USER_GUIDE.md) for comprehensive examples including:
586
+ - Basic PFAS parsing and analysis
587
+ - Fingerprint generation for machine learning
588
+ - Custom configuration files
589
+ - Batch processing
590
+ - Integration with pandas and scikit-learn
591
+
592
+ ## Summary of changes by version
593
+
594
+ - **Version 3.2.2**: Fixed polyhalogenated alkyl components matching less than 2 halogens. Added option to pass 'halogens' to formula constraints. Added options to include component-wide formula constraints (on dist-1 neighbours from matched C-only components).
595
+
596
+ - **Version 3.2.1**: Added n-spacer metric for telomers and ring size for aryl and cyclic groups. These can be used in the embeddings.
597
+
598
+ - **Version 3.2.0**: Use of BDE for computing graph resistance.
599
+
600
+ - **Version 3.1.4**: Changed fingerprint parameters to molecule wide and component wide, with pre-configuration for best combinations.
601
+
602
+ - **Version 3.1.0**: Added support for other halogens, changed names to be more generic (with some support for backward compatibility). Added component smarts for other halogens, cyclic and alkyl components.
603
+
604
+ - **Version 2.2.4 (Feb 2026)**: Advanced fingerprint analysis with dimensionality reduction (PCA, kernel-PCA, t-SNE, UMAP), KL divergence comparison for dataset similarity assessment, and SQL persistence for results. Added molecule prioritization tool for screening applications, ranking by similarity to reference lists or by intrinsic fluorination properties. Introduced `PFASEmbeddingSet` and `PFASEmbedding` with comprehensive analysis methods, automated plot generation, and extensive documentation.
605
+
606
+ - **Version 2.2.3 (Feb 2026)**: Added `PFASEmbeddingSet` container to offer easier plotting and summarising capabilities for results.
607
+
608
+ - **Version 2.2 (Feb 2026)**: Added linked_smarts option to specify a restriction on path between smarts groups and fluorinated component. Added new PFASgroups (telomers). **v2.2.2** Fixed telomers and added examples and counter-examples to each PFASgroup. Removed boundary O in fluorinated components (for both Per and Polyalkyl components).
609
+
610
+ - **Version 2.1 (Jan 2026)**: Added support for multiple smarts, with individual minimum count, per PFASgroup.
611
+
612
+ - **Version 2.0 (Jan 2026)**: Major expansion of graph‑based component metrics, new coverage statistics, schema updates, and richer per‑component outputs.
613
+
614
+ ### Version 2.2.4 (February 2026) - Advanced Fingerprint Analysis
615
+
616
+ Major enhancement adding comprehensive dimensionality reduction and statistical comparison capabilities:
617
+
618
+ **New Features:**
619
+ - **PFASEmbeddingSet / PFASEmbedding**: Unified result container with flexible embedding generation
620
+ - **Dimensionality Reduction**: PCA, kernel-PCA, t-SNE, and UMAP with automatic plotting
621
+ - **Statistical Comparison**: KL divergence for comparing dataset compositions
622
+ - **Database Persistence**: SQL save/load for results
623
+ - **Molecule Prioritization**: Screening and ranking tool for PFAS datasets
624
+ - **Comprehensive Documentation**: Complete API reference, examples, and 100+ tests
625
+
626
+ **Key Methods:**
627
+ - `PFASEmbeddingSet.to_array()`: Generate numeric embedding matrix from parsed results
628
+ - `PFASEmbeddingSet.column_names()`: Column labels matching `to_array()` output
629
+ - `PFASEmbeddingSet.perform_pca()`: Principal Component Analysis
630
+ - `PFASEmbeddingSet.perform_kernel_pca()`: Non-linear kernel PCA
631
+ - `PFASEmbeddingSet.perform_tsne()`: t-SNE visualization
632
+ - `PFASEmbeddingSet.perform_umap()`: Fast UMAP dimensionality reduction
633
+ - `PFASEmbeddingSet.compare_kld()`: Dataset similarity via KL divergence
634
+ - `prioritise_molecules()`: Rank molecules by similarity or fluorination properties
635
+ - `PFASEmbeddingSet.to_sql()`: Persist results to a SQLite/PostgreSQL database
636
+
637
+ **Prioritization Strategies:**
638
+ - **Reference-based**: Rank by distributional similarity to known PFAS (e.g., persistent chemicals)
639
+ - **Intrinsic properties**: Score by fluorination characteristics (total F content, chain length)
640
+ - **Flexible weighting**: Tune parameters for different screening objectives
641
+
642
+ **Use Cases:**
643
+ - Exploratory data analysis of PFAS inventories
644
+ - Database comparison and compositional analysis
645
+ - Cluster identification and pattern recognition
646
+ - Machine learning preprocessing
647
+ - Chemical space visualization
648
+ - Priority screening for environmental monitoring
649
+ - Regulatory watchlist generation
650
+
651
+ See `docs/ResultsFingerprint_Guide.md`, `docs/prioritization.rst`, `examples/results_fingerprint_analysis.py`, and `examples/prioritization_examples.py` for details.
652
+
653
+ ### Version 2.2.3 (February 2026) - PFASEmbeddingSet Container
654
+
655
+ **New Features:**
656
+ - `PFASEmbeddingSet` container with visualization helpers
657
+ - Enhanced component plotting utilities
658
+ - Improved documentation and examples
659
+
660
+ ### Version 2.0 (January 2026) - Comprehensive Graph Metrics
661
+
662
+ Major enhancement adding comprehensive NetworkX graph theory metrics for detailed component analysis:
663
+
664
+ **New Features:**
665
+ - **Component-Level Metrics**: Each fluorinated component now includes 15+ graph metrics:
666
+ - `diameter` and `radius` - Graph eccentricity bounds
667
+ - `center`, `periphery`, `barycenter` - Structural node sets
668
+ - `effective_graph_resistance` - Sum of resistance distances
669
+ - `component_fraction` - Fraction of molecule covered by component (includes all attached H, F, Cl, Br, I)
670
+ - Distance metrics from functional groups to structural features
671
+ - **Molecular Coverage Metrics**: New fraction-based metrics quantify fluorination extent:
672
+ - `mean_component_fraction` - Average coverage per component
673
+ - `total_components_fraction` - Total coverage by union of all components (accounts for overlaps)
674
+ - **Summary Statistics**: Aggregated metrics across all components per PFAS group
675
+ - **Enhanced Database Models**: New `Components` model stores individual component data with all metrics
676
+ - **Improved Analysis**: Better understanding of molecular topology, branching, functional group positioning, and fluorination extent
677
+
678
+ **Breaking Changes:**
679
+ - `parse_mols` output now includes additional summary metric fields (`mean_diameter`, `mean_radius`, etc.)
680
+ - Database schema changes require migration (see `DATABASE_MIGRATION_GUIDE.md`)
681
+
682
+ **Metrics Explained:**
683
+ - `branching` (0-1): Measures linearity (1.0 = linear, 0.0 = highly branched) - renamed from "eccentricity"
684
+ - `mean_eccentricity`, `median_eccentricity`: Graph-theoretic eccentricity statistics for component nodes
685
+ - `smarts_centrality` (0-1): Functional group position (1.0 = central, 0.0 = peripheral)
686
+ - `n_spacer` (int ≥ 0): Fluorotelomer CH₂ spacer length — the "m" in "m:n" telomer notation; 0 for all non-telomeric groups
687
+ - `ring_size` (int ≥ 0): Smallest ring overlapping the matched component; 0 for acyclic chains, 5 for azoles/furans, 6 for benzene/cyclohexane derivatives
688
+ - `component_fraction` (0-1): Fraction of total molecule atoms in this component (includes all attached atoms)
689
+ - `total_components_fraction` (0-1): Fraction of molecule covered by union of all components
690
+ - `diameter`: Maximum distance between any two atoms in component
691
+ - `radius`: Minimum eccentricity across component nodes
692
+ - `barycenter`: Nodes minimizing total distance to all other nodes
693
+ - `center`: Nodes with minimum eccentricity
694
+ - `periphery`: Nodes with maximum eccentricity
695
+
696
+ See `COMPREHENSIVE_METRICS_SUMMARY.md` for complete documentation.
697
+
698
+ - **Version 1.x**: Shift to component‑based analysis with improved SMARTS matching and better handling of branched/cyclic structures.
699
+
700
+ ### Version 1.x - Component-Based Analysis
701
+
702
+ - Replaced chain-finding with connected component analysis
703
+ - Added support for branched and cyclic structures
704
+ - Improved SMARTS pattern matching for diverse PFAS classes
705
+
706
+ ### Version 0.x - Path-Based Analysis
707
+
708
+ - Find SMARTS match connected to either a second SMARTS or a default path-related SMARTS using networkx shortest_path.
709
+
710
+
711
+
712
+ ## Licence
713
+ <a rel="license" href="http://creativecommons.org/licenses/by-nd/4.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by-nd/4.0/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-nd/4.0/">Creative Commons Attribution-NoDerivatives 4.0 International License</a>.
714
+
715
+ Contact me in case you want an exception to the No Derivatives term.
716
+
717
+ ## Acknowledgments
718
+ This project is part of the [ZeroPM project](https://zeropm.eu/) (WP2) and has received funding from the European Union’s Horizon 2020 research and innovation programme under grant agreement No 101036756. This work was developed at the [Department of Environmental Science](https://aces.su.se) at Stockholm University.<br />
719
+
720
+
721
+ <img alt="EU logo" src="https://zeropm.eu/wp-content/uploads/2021/12/flag_yellow_low.jpg" width=100/> <a rel='zeropm_web' href="https://zeropm.eu/"/><img alt="zeropm logo" src="https://zeropm.eu/wp-content/uploads/2022/01/ZeroPM-logo.png" width=250 /></a><a rel='zeropm_web' href="https://su.se/"/><img alt="zeropm logo" src="https://eu01web.zoom.us/account/branding/p/5065401a-9915-4baa-9c16-665dcd743470.png" width=200 /></a>
722
+
723
+ [![Powered by RDKit](https://img.shields.io/badge/Powered%20by-RDKit-3838ff.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAFVBMVEXc3NwUFP8UPP9kZP+MjP+0tP////9ZXZotAAAAAXRSTlMAQObYZgAAAAFiS0dEBmFmuH0AAAAHdElNRQfmAwsPGi+MyC9RAAAAQElEQVQI12NgQABGQUEBMENISUkRLKBsbGwEEhIyBgJFsICLC0iIUdnExcUZwnANQWfApKCK4doRBsKtQFgKAQC5Ww1JEHSEkAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wMy0xMVQxNToyNjo0NyswMDowMDzr2J4AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDMtMTFUMTU6MjY6NDcrMDA6MDBNtmAiAAAAAElFTkSuQmCC)](https://www.rdkit.org/)
724
+