PFASGroups 3.4.0__tar.gz → 3.4.2__tar.gz

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.
Files changed (82) hide show
  1. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/HalogenGroupModel.py +14 -1
  2. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/__init__.py +10 -0
  3. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/data/PFAS_definitions_smarts.json +29 -6
  4. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/data/component_smarts_halogens.json +17 -2
  5. pfasgroups-3.4.2/PFASGroups/group_features.py +393 -0
  6. {pfasgroups-3.4.0 → pfasgroups-3.4.2/PFASGroups.egg-info}/PKG-INFO +43 -2
  7. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups.egg-info/SOURCES.txt +1 -0
  8. {pfasgroups-3.4.0/PFASGroups.egg-info → pfasgroups-3.4.2}/PKG-INFO +43 -2
  9. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/README.md +42 -1
  10. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/data/help.html +37 -37
  11. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/pyproject.toml +1 -1
  12. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/setup.cfg +2 -2
  13. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/HalogenGroups/__init__.py +0 -0
  14. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/ComponentsSolverModel.py +0 -0
  15. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/PFASDefinitionModel.py +0 -0
  16. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/PFASEmbeddings.py +0 -0
  17. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/cli.py +0 -0
  18. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/core.py +0 -0
  19. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/data/Halogen_groups_smarts.json +0 -0
  20. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/data/component_smarts.json +0 -0
  21. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/data/diatomic_bonds_dict.json +0 -0
  22. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/draw_mols.py +0 -0
  23. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/embeddings.py +0 -0
  24. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/fragmentation.py +0 -0
  25. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/generate_homologues.py +0 -0
  26. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/generate_mol.py +0 -0
  27. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/generate_paper_figures.py +0 -0
  28. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/getter.py +0 -0
  29. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/homologue_series.py +0 -0
  30. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/parser.py +0 -0
  31. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups/prioritise.py +0 -0
  32. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups.egg-info/dependency_links.txt +0 -0
  33. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups.egg-info/entry_points.txt +0 -0
  34. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups.egg-info/requires.txt +0 -0
  35. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/PFASGroups.egg-info/top_level.txt +0 -0
  36. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/__init__.py +0 -0
  37. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/__main__.py +0 -0
  38. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/app.py +0 -0
  39. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/style.py +0 -0
  40. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/tabs/__init__.py +0 -0
  41. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/tabs/chemspace_tab.py +0 -0
  42. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/tabs/classify_tab.py +0 -0
  43. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/tabs/definition_tester_tab.py +0 -0
  44. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/tabs/help_tab.py +0 -0
  45. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/tabs/modelling_tab.py +0 -0
  46. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/tabs/prioritise_tab.py +0 -0
  47. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/tabs/results_tab.py +0 -0
  48. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/utils/__init__.py +0 -0
  49. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/utils/chemspace.py +0 -0
  50. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/utils/export_dialog.py +0 -0
  51. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/utils/fingerprints.py +0 -0
  52. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/utils/io_readers.py +0 -0
  53. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/utils/modelling.py +0 -0
  54. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/utils/mol_renderer.py +0 -0
  55. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/gui/workers.py +0 -0
  56. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_bde_resistance.py +0 -0
  57. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_component_fractions.py +0 -0
  58. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_component_ratios.py +0 -0
  59. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_comprehensive_metrics.py +0 -0
  60. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_database_integration.py +0 -0
  61. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_definition_comparison.py +0 -0
  62. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_fingerprints_vs_txppfas.py +0 -0
  63. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_fraction_analysis.py +0 -0
  64. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_generate_homologues.py +0 -0
  65. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_halogen_groups_smarts.py +0 -0
  66. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_linker_smarts.py +0 -0
  67. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_metrics.py +0 -0
  68. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_metrics_detailed.py +0 -0
  69. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_n_spacer_ring_size.py +0 -0
  70. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_parser_wildcard.py +0 -0
  71. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_pfasstructv5.py +0 -0
  72. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_prioritise.py +0 -0
  73. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_readme_and_docs_examples.py +0 -0
  74. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_results_fingerprint.py +0 -0
  75. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_results_model.py +0 -0
  76. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_results_sql.py +0 -0
  77. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_run_groups_definitions_tests.py +0 -0
  78. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_smarts_atom_count.py +0 -0
  79. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_smarts_matching.py +0 -0
  80. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_smarts_precompute.py +0 -0
  81. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_summary.py +0 -0
  82. {pfasgroups-3.4.0 → pfasgroups-3.4.2}/tests/test_telomer_validation.py +0 -0
@@ -156,8 +156,15 @@ class HalogenGroup():
156
156
  # prepare halogens (accepts list, str or None)
157
157
  if isinstance(self.componentHalogens,list) and len(set(self.componentHalogens).intersection(['F','Cl','Br','I','H'])) == 0:
158
158
  raise ValueError(f"Invalid componentHalogens for HalogenGroup '{self.name}' (ID: {self.id})")
159
+ _halogens_inferred = False
159
160
  if self.componentHalogens is None:
160
- self.componentHalogens = ['F','Cl','Br','I']
161
+ # Derive from whichever halogens have component SMARTS defined in the
162
+ # available componentSmartss. Fall back to real halogens only (no H)
163
+ # when the data provides no guidance.
164
+ _hal_order = ['F', 'Cl', 'Br', 'I', 'H']
165
+ _inferred = [h for h in _hal_order if h in componentSmarts_dict]
166
+ self.componentHalogens = _inferred if _inferred else ['F', 'Cl', 'Br', 'I']
167
+ _halogens_inferred = True
161
168
  elif isinstance(self.componentHalogens, str) and self.componentHalogens in ['F','Cl','Br','I','H']:
162
169
  self.componentHalogens = [self.componentHalogens]
163
170
  if self.componentSaturation is None:
@@ -172,11 +179,17 @@ class HalogenGroup():
172
179
  self.componentForm = 'alkyl'
173
180
  for halogen in self.componentHalogens:
174
181
  if halogen not in componentSmarts_dict:
182
+ if _halogens_inferred:
183
+ continue
175
184
  raise ValueError(f"No component SMARTS available for halogen '{halogen}' in HalogenGroup '{self.name}' (ID: {self.id})")
176
185
  if self.componentForm not in componentSmarts_dict[halogen]:
186
+ if _halogens_inferred:
187
+ continue
177
188
  raise ValueError(f"No component SMARTS available for form '{self.componentForm}' and halogen '{halogen}' in HalogenGroup '{self.name}' (ID: {self.id})")
178
189
  for saturation in self.componentSaturation:
179
190
  if saturation not in componentSmarts_dict[halogen][self.componentForm]:
191
+ if _halogens_inferred:
192
+ continue
180
193
  raise ValueError(f"No component SMARTS available for saturation '{saturation}', form '{self.componentForm}', halogen '{halogen}' in HalogenGroup '{self.name}' (ID: {self.id})")
181
194
  new_CS.append(componentSmarts_dict[halogen][self.componentForm][saturation])
182
195
  self.componentSmarts = new_CS
@@ -15,7 +15,17 @@ from .fragmentation import generate_degradation_products
15
15
  # PFASEmbedding (dict subclass, primary) must be imported after embeddings to take precedence
16
16
  from .PFASEmbeddings import PFASEmbedding, PFASEmbeddingSet, EmbeddingArray, ResultsModel, MoleculeResult, generate_fingerprint
17
17
  from .prioritise import prioritise_molecules, prioritize_molecules, get_priority_statistics
18
+ from .group_features import (
19
+ extract_group_features, GroupFeatureResult,
20
+ PER_GROUP_IDS, POLY_GROUP_IDS, HALOGENS_ORDER,
21
+ GENERIC_GROUP_VOCAB, GENERIC_GROUP_NAMES,
22
+ )
18
23
  __version__ = "3.2.0"
19
24
  __all__ = ['HalogenGroup', 'PFASDefinition', 'parse_smiles', 'parse_mols','parse_mol', 'parse_groups_in_mol', 'parse_from_database', 'setup_halogen_groups_database', 'plot_HalogenGroups', 'plot_mol','plot_mols', 'FINGERPRINT_PRESETS', 'PFASFingerprint', 'generate_fingerprint', 'get_compiled_componentSMARTSs', 'get_componentSMARTSs', 'get_HalogenGroups', 'get_compiled_HalogenGroups', 'get_compiled_PFASGroups', 'get_PFASDefinitions' ,'ComponentsSolver', 'generate_homologues', 'generate_degradation_products',"rdkit_disable_log","load_HalogenGroups", "HALOGEN_GROUPS_FILE", "_match_wildcard_groups_in_mol"]
20
25
  __all__.extend(['PFASEmbedding', 'PFASEmbeddingSet', 'EmbeddingArray', 'ResultsModel', 'MoleculeResult', 'prioritise_molecules', 'prioritize_molecules', 'get_priority_statistics'])
21
26
  __all__.extend(['HomologueSeries', 'HomologueEntry'])
27
+ __all__.extend([
28
+ 'extract_group_features', 'GroupFeatureResult',
29
+ 'PER_GROUP_IDS', 'POLY_GROUP_IDS', 'HALOGENS_ORDER',
30
+ 'GENERIC_GROUP_VOCAB', 'GENERIC_GROUP_NAMES',
31
+ ])
@@ -63,12 +63,12 @@
63
63
  "id": 3,
64
64
  "name": "OPPT 2023",
65
65
  "smarts": [
66
- "[#6H0X4](F)(F)[#6H0X4](F)",
67
- "[#6](F)(F)([F,#8,#6H0X4])[#8][#6](F)(F)[F,#8,#6H0X4]",
68
- "[#6](F)(F)(F)[#6]([#6](F)(F)F)([F,#6H0X4])[F,#6H0X4]"
66
+ "[#6X4](F)(F)[#6X4](F)",
67
+ "[#6](F)(F)([F,#8,#6X4])[#8][#6](F)(F)[F,#8,#6X4]",
68
+ "[#6](F)(F)(F)[#6]([#6](F)(F)F)([F,#6X4])[F,#6X4]"
69
69
  ],
70
70
  "fluorineRatio": null,
71
- "description": "Three inclusion criteria.",
71
+ "description": "Three inclusion criteria. R groups are unconstrained per the final 2023 rule (40 CFR 705.3) -- the 2021 proposal's \"none of the R groups can be hydrogen\" language was dropped when finalized, so R positions (and the non-defining valence on the backbone CF2/CF carbons) may be H. Validated against EPA's published PFAS8a7v3 CompTox list: 99.96% coverage (13,035/13,040) with H unconstrained, vs 91.50% with H0 enforced.",
72
72
  "test": {
73
73
  "category": "definition",
74
74
  "examples": {
@@ -77,13 +77,13 @@
77
77
  "C(F)(F)C(F)(F)C(F)(F)C",
78
78
  "C(F)(F)(F)C(F)(F)OC(F)(F)C(F)(F)F",
79
79
  "C(F)(F)(C(F)(F)F)C(F)(F)C(F)(F)F",
80
- "C(F)(F)(F)C(OC)(F)C(F)(F)F"
80
+ "C(F)(F)(F)C(OC)(F)C(F)(F)F",
81
+ "FC(F)C(F)F"
81
82
  ],
82
83
  "negatives": [
83
84
  "CCCCCC",
84
85
  "C(F)CCC",
85
86
  "CC(F)(F)C",
86
- "FC(F)C(F)F",
87
87
  "C1=CC=CC=C1"
88
88
  ]
89
89
  }
@@ -166,5 +166,28 @@
166
166
  ]
167
167
  }
168
168
  }
169
+ },
170
+ {
171
+ "id": 6,
172
+ "name": "OPPT 2021",
173
+ "smarts": [
174
+ "[#6H0X4](F)(F)[#6H0X4](F)"
175
+ ],
176
+ "fluorineRatio": null,
177
+ "description": "R-(CF2)-CF(R')R'', where the CF2 and CF moieties are saturated carbons and none of the R groups can be hydrogen (2021 proposed definition; superseded by OPPT 2023, which drops this H restriction).",
178
+ "test": {
179
+ "category": "definition",
180
+ "examples": {
181
+ "positives": [
182
+ "C(F)(F)(F)C(F)(F)C(F)(F)F"
183
+ ],
184
+ "negatives": [
185
+ "CCCCCC",
186
+ "C(F)CCC",
187
+ "CC(F)(F)C",
188
+ "C1=CC=CC=C1"
189
+ ]
190
+ }
191
+ }
169
192
  }
170
193
  ]
@@ -143,8 +143,23 @@
143
143
  "alkyl": {
144
144
  "per": {
145
145
  "name": "Alkyl",
146
- "component": "[CH2]",
147
- "end": "[CH3]"
146
+ "component": "[CH2!$([#6][F,Cl,Br,I]),CH3$([CH3][#6])!$([#6][F,Cl,Br,I])]",
147
+ "end": "[CH3]",
148
+ "constraints": {"gte": {"H": 2}}
149
+ }
150
+ },
151
+ "cyclic": {
152
+ "per": {
153
+ "name": "Cyclic alkyl",
154
+ "component": "[#6aX4R$([#6aR]([#6aR!$([#6][F,Cl,Br,I])])([#6aR!$([#6][F,Cl,Br,I])])[!$([#6][F,Cl,Br,I])][!$([#6][F,Cl,Br,I])])]",
155
+ "constraints": {"gte": {"H": 2}}
156
+ }
157
+ },
158
+ "aryl": {
159
+ "per": {
160
+ "name": "Aryl",
161
+ "component": "[#6aX3R$([#6aR]([#6aR!$([#6][F,Cl,Br,I])])[#6aR!$([#6][F,Cl,Br,I])])!$([#6][F,Cl,Br,I])]",
162
+ "constraints": {"gte": {"H": 1}}
148
163
  }
149
164
  }
150
165
  }
@@ -0,0 +1,393 @@
1
+ """Structured feature extraction from PFASGroups halogen group detection.
2
+
3
+ This module provides :func:`extract_group_features`, a single public entry
4
+ point that runs the necessary :func:`~PFASGroups.parser.parse_mol` calls and
5
+ returns a :class:`GroupFeatureResult` with four named feature dictionaries
6
+ covering the following four group categories:
7
+
8
+ - **Polyhalogenated groups** (ids 35, 38, 45): aggregate match counts
9
+ - **Perhalogenated groups** (ids 34, 37, 44): per-halogen max component sizes
10
+ - **Alkyl chain (H pseudo-halogen)** aggregates: convenience view of the H
11
+ column of perhalogenated results
12
+ - **Generic functional groups** (ids 29-76): wildcard bag-of-groups counts
13
+
14
+ The six component groups (34/35/37/38/44/45) are excluded from wildcard
15
+ matching by design, so they never appear in the generic-group counts.
16
+
17
+ Example
18
+ -------
19
+ >>> from rdkit import Chem
20
+ >>> from PFASGroups import extract_group_features
21
+ >>> mol = Chem.MolFromSmiles("FC(F)(F)C(F)(F)C(F)(F)C(=O)O") # PFOA-like
22
+ >>> r = extract_group_features(mol)
23
+ >>> r.poly_counts
24
+ {'poly_alkyl': 1.0, 'poly_aryl': 0.0, 'poly_cyclic': 0.0}
25
+ >>> r.per_halogen_sizes['g34_F'] # longest perfluoroalkyl component (carbons)
26
+ 4.0
27
+ >>> r.h_chain_sizes
28
+ {'alkyl_H': 0.0, 'aryl_H': 0.0, 'cyclic_H': 0.0}
29
+ >>> len(r.to_array()) # 3 + 15 + 48
30
+ 66
31
+ """
32
+ from __future__ import annotations
33
+
34
+ from dataclasses import dataclass, field
35
+ from typing import Union
36
+
37
+ import numpy as np
38
+ from rdkit import Chem
39
+
40
+ from .parser import parse_mol as _parse_mol # type: ignore[attr-defined]
41
+ from .getter import get_compiled_HalogenGroups as _get_compiled_HalogenGroups
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Public constants
45
+ # ---------------------------------------------------------------------------
46
+
47
+ #: Group IDs for perhalogenated groups (alkyl, aryl, cyclic).
48
+ #: These groups require *all* halogen-bearing carbons to carry only one halogen
49
+ #: type. Each match is attributed to a single halogen (or H for alkyl chains).
50
+ PER_GROUP_IDS: list[int] = [34, 37, 44]
51
+
52
+ #: Group IDs for polyhalogenated groups (alkyl, aryl, cyclic).
53
+ #: These groups allow a *mixture* of halogens and/or partial halogenation.
54
+ #: Match counts are aggregated across all halogens.
55
+ POLY_GROUP_IDS: list[int] = [35, 38, 45]
56
+
57
+ #: Halogen types recognised in :attr:`GroupFeatureResult.per_halogen_sizes`.
58
+ #: ``'H'`` is the pseudo-halogen used to detect un-substituted alkyl chains
59
+ #: (perhalogenated-alkyl, group 34) via :class:`~PFASGroups.ComponentsSolverModel.ComponentsSolver`.
60
+ HALOGENS_ORDER: list[str] = ["F", "Cl", "Br", "I", "H"]
61
+
62
+ #: Ordered list of group IDs covered by :attr:`GroupFeatureResult.generic_groups`.
63
+ #: Ids {34, 35, 37, 38, 44, 45} are excluded from wildcard matching and will
64
+ #: always be zero in this vector.
65
+ GENERIC_GROUP_VOCAB: list[int] = list(range(29, 77)) # 48 entries
66
+
67
+ #: Human-readable names for every group id in :data:`GENERIC_GROUP_VOCAB`.
68
+ GENERIC_GROUP_NAMES: dict[int, str] = {
69
+ 29: "acrylate",
70
+ 30: "acyl halide",
71
+ 31: "alcohol",
72
+ 32: "aldehyde",
73
+ 33: "alkene",
74
+ 34: "perhalogenated alkyl",
75
+ 35: "polyhalogenated alkyl",
76
+ 36: "alkyne",
77
+ 37: "perhalogenated aryl compounds",
78
+ 38: "polyhalogenated aryl compounds",
79
+ 39: "benzodioxole",
80
+ 40: "benzoyl peroxydes",
81
+ 41: "bromide",
82
+ 42: "carboxylic acid",
83
+ 43: "chloride",
84
+ 44: "perhalogenated cyclic compounds",
85
+ 45: "polyhalogenated cyclic compounds",
86
+ 46: "ester",
87
+ 47: "ether",
88
+ 48: "fluoride",
89
+ 49: "glucuronate",
90
+ 50: "iodide",
91
+ 51: "ketone",
92
+ 52: "methacrylate",
93
+ 53: "peroxydes",
94
+ 54: "side-chain aromatics",
95
+ 55: "sulfenic acid",
96
+ 56: "sulfenyl halide",
97
+ 57: "sulfinic acid",
98
+ 58: "sulfinyl amido sulfonic acid",
99
+ 59: "sulfonamide",
100
+ 60: "sulfonamidoethanol",
101
+ 61: "sulfonic acid",
102
+ 62: "sulfonyl halide",
103
+ 63: "sulfonyl propanoic acid",
104
+ 64: "sulfuric acid",
105
+ 65: "thioester keto dicarboxylic acid",
106
+ 66: "thiocyanic acid",
107
+ 67: "phosphinic acid",
108
+ 68: "phosphonic acid",
109
+ 69: "amide",
110
+ 70: "amine",
111
+ 71: "heterocyclic azine",
112
+ 72: "heterocyclic azole",
113
+ 73: "betaine",
114
+ 74: "glycine",
115
+ 75: "trichlorosilane",
116
+ 76: "silane",
117
+ }
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Internal state
121
+ # ---------------------------------------------------------------------------
122
+
123
+ _COMPONENT_GROUPS: list | None = None # cached list of HalogenGroups {34..45}
124
+ _GENERIC_GROUP_INDEX: dict[int, int] = {gid: i for i, gid in enumerate(GENERIC_GROUP_VOCAB)}
125
+ _HAL_TO_COL: dict[str, int] = {h: i for i, h in enumerate(HALOGENS_ORDER)}
126
+ _PER_GID_TO_ROW: dict[int, int] = {34: 0, 37: 1, 44: 2}
127
+ _POLY_GID_TO_IDX: dict[int, int] = {35: 0, 38: 1, 45: 2}
128
+
129
+ # Short labels used in h_chain_sizes
130
+ _H_CHAIN_KEYS: list[str] = ["alkyl_H", "aryl_H", "cyclic_H"]
131
+ # Corresponding per_halogen_sizes keys (H column of each per group)
132
+ _H_CHAIN_PHS_KEYS: list[str] = ["g34_H", "g37_H", "g44_H"]
133
+
134
+
135
+ def _get_component_groups() -> list:
136
+ """Return a cached list of :class:`~PFASGroups.HalogenGroup` objects with ids in ``{34,35,37,38,44,45}``."""
137
+ global _COMPONENT_GROUPS
138
+ if _COMPONENT_GROUPS is None:
139
+ _ids = {34, 35, 37, 38, 44, 45}
140
+ _COMPONENT_GROUPS = [g for g in _get_compiled_HalogenGroups() if g.id in _ids]
141
+ return _COMPONENT_GROUPS
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # GroupFeatureResult
146
+ # ---------------------------------------------------------------------------
147
+
148
+ @dataclass
149
+ class GroupFeatureResult:
150
+ """Structured feature extraction result for a single molecule.
151
+
152
+ This dataclass groups features from two :func:`~PFASGroups.parser.parse_mol`
153
+ calls into four semantically distinct dictionaries. It is returned by
154
+ :func:`extract_group_features`.
155
+
156
+ Attributes
157
+ ----------
158
+ poly_counts : dict[str, float]
159
+ Match counts for **polyhalogenated** groups (ids 35, 38, 45), aggregated
160
+ across all halogens (F, Cl, Br, I, H). Keys: ``'poly_alkyl'``,
161
+ ``'poly_aryl'``, ``'poly_cyclic'``.
162
+
163
+ per_halogen_sizes : dict[str, float]
164
+ Maximum carbon-component size for **perhalogenated** groups (ids 34, 37,
165
+ 44) resolved per halogen. Keys follow the pattern ``'g{id}_{hal}'``
166
+ where *id* is in ``{34, 37, 44}`` and *hal* is in
167
+ ``['F', 'Cl', 'Br', 'I', 'H']``. Zero when no match is found.
168
+
169
+ h_chain_sizes : dict[str, float]
170
+ Convenience view of the ``'H'`` column of :attr:`per_halogen_sizes`:
171
+ the largest un-substituted alkyl / aryl / cyclic component detected via
172
+ the H pseudo-halogen mechanism. Keys: ``'alkyl_H'``, ``'aryl_H'``,
173
+ ``'cyclic_H'``. **Not included in** :meth:`to_array`.
174
+
175
+ generic_groups : dict[str, float]
176
+ Wildcard functional-group match counts for group ids 29-76. Keys:
177
+ ``'g29'`` … ``'g76'``. Ids ``{34, 35, 37, 38, 44, 45}`` are excluded
178
+ from wildcard matching and will always be zero.
179
+ """
180
+
181
+ poly_counts: dict[str, float] = field(default_factory=dict)
182
+ per_halogen_sizes: dict[str, float] = field(default_factory=dict)
183
+ h_chain_sizes: dict[str, float] = field(default_factory=dict)
184
+ generic_groups: dict[str, float] = field(default_factory=dict)
185
+
186
+ def to_array(self) -> np.ndarray:
187
+ """Return a fixed-length float32 array of shape ``(66,)``.
188
+
189
+ Layout::
190
+
191
+ [0:3] poly_counts (poly_alkyl, poly_aryl, poly_cyclic)
192
+ [3:18] per_halogen_sizes (g34_F…g44_H, 3 groups × 5 halogens)
193
+ [18:66] generic_groups (g29…g76, 48 entries)
194
+
195
+ Note
196
+ ----
197
+ :attr:`h_chain_sizes` is excluded from this array (it is a strict
198
+ subset of :attr:`per_halogen_sizes`).
199
+ """
200
+ poly = np.array(
201
+ [self.poly_counts[k] for k in ("poly_alkyl", "poly_aryl", "poly_cyclic")],
202
+ dtype=np.float32,
203
+ )
204
+ per_keys = [
205
+ f"g{gid}_{h}"
206
+ for gid in PER_GROUP_IDS
207
+ for h in HALOGENS_ORDER
208
+ ]
209
+ per = np.array([self.per_halogen_sizes[k] for k in per_keys], dtype=np.float32)
210
+ gen = np.array(
211
+ [self.generic_groups[f"g{gid}"] for gid in GENERIC_GROUP_VOCAB],
212
+ dtype=np.float32,
213
+ )
214
+ return np.concatenate([poly, per, gen])
215
+
216
+ def feature_names(self) -> list[str]:
217
+ """Return the 66 feature names in the same order as :meth:`to_array`.
218
+
219
+ Returns
220
+ -------
221
+ list[str]
222
+ Labels: ``['poly_alkyl', 'poly_aryl', 'poly_cyclic',
223
+ 'g34_F', 'g34_Cl', …, 'g44_H', 'g29', 'g30', …, 'g76']``.
224
+ """
225
+ poly_names = ["poly_alkyl", "poly_aryl", "poly_cyclic"]
226
+ per_names = [
227
+ f"g{gid}_{h}"
228
+ for gid in PER_GROUP_IDS
229
+ for h in HALOGENS_ORDER
230
+ ]
231
+ gen_names = [f"g{gid}" for gid in GENERIC_GROUP_VOCAB]
232
+ return poly_names + per_names + gen_names
233
+
234
+ def __repr__(self) -> str:
235
+ nz_poly = sum(1 for v in self.poly_counts.values() if v)
236
+ nz_per = sum(1 for v in self.per_halogen_sizes.values() if v)
237
+ nz_gen = sum(1 for v in self.generic_groups.values() if v)
238
+ return (
239
+ f"GroupFeatureResult("
240
+ f"poly_counts={nz_poly} nonzero, "
241
+ f"per_halogen_sizes={nz_per} nonzero, "
242
+ f"generic_groups={nz_gen} nonzero)"
243
+ )
244
+
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # Public API
248
+ # ---------------------------------------------------------------------------
249
+
250
+ def extract_group_features(
251
+ mol: Union[Chem.Mol, str],
252
+ ) -> GroupFeatureResult:
253
+ """Extract structured halogen-group features for a single molecule.
254
+
255
+ Two :func:`~PFASGroups.parser.parse_mol` calls are made internally:
256
+
257
+ 1. ``halogens=['H','F','Cl','Br','I']`` with the six component groups
258
+ (ids 34, 35, 37, 38, 44, 45) — populates :attr:`~GroupFeatureResult.poly_counts`
259
+ and :attr:`~GroupFeatureResult.per_halogen_sizes`.
260
+ 2. ``halogens=['*']`` (wildcard) — populates
261
+ :attr:`~GroupFeatureResult.generic_groups`.
262
+
263
+ Parameters
264
+ ----------
265
+ mol : rdkit.Chem.Mol or str
266
+ RDKit molecule object or a SMILES string.
267
+
268
+ Returns
269
+ -------
270
+ GroupFeatureResult
271
+ Populated result object. Call :meth:`~GroupFeatureResult.to_array` to
272
+ obtain a fixed-length float32 array of shape ``(66,)``.
273
+
274
+ Raises
275
+ ------
276
+ ValueError
277
+ If a SMILES string cannot be parsed by RDKit.
278
+ RuntimeError
279
+ If an unexpected error occurs during :func:`parse_mol`.
280
+
281
+ Examples
282
+ --------
283
+ >>> from rdkit import Chem
284
+ >>> from PFASGroups import extract_group_features
285
+ >>> pfoa = Chem.MolFromSmiles("FC(F)(F)C(F)(F)C(F)(F)C(=O)O")
286
+ >>> r = extract_group_features(pfoa)
287
+ >>> r.poly_counts
288
+ {'poly_alkyl': 1.0, 'poly_aryl': 0.0, 'poly_cyclic': 0.0}
289
+ >>> r.per_halogen_sizes['g34_F']
290
+ 4.0
291
+ >>> r.h_chain_sizes
292
+ {'alkyl_H': 0.0, 'aryl_H': 0.0, 'cyclic_H': 0.0}
293
+ >>> arr = r.to_array()
294
+ >>> arr.shape
295
+ (66,)
296
+ >>> # Octane — only H alkyl chain detected
297
+ >>> octane = Chem.MolFromSmiles("CCCCCCCC")
298
+ >>> r2 = extract_group_features(octane)
299
+ >>> r2.h_chain_sizes['alkyl_H']
300
+ 6.0
301
+ """
302
+ if isinstance(mol, str):
303
+ mol = Chem.MolFromSmiles(mol)
304
+ if mol is None:
305
+ raise ValueError(f"RDKit could not parse SMILES string.")
306
+
307
+ # --- Call 1: component groups with all halogens including H ---
308
+ emb_chain = _parse_mol(
309
+ mol,
310
+ halogens=["H", "F", "Cl", "Br", "I"],
311
+ pfas_groups=_get_component_groups(),
312
+ compute_component_metrics=False,
313
+ )
314
+
315
+ # --- Call 2: wildcard generic functional groups ---
316
+ emb_wc = _parse_mol(
317
+ mol,
318
+ halogens=["*"],
319
+ compute_component_metrics=False,
320
+ )
321
+
322
+ # ------------------------------------------------------------------
323
+ # Build poly_counts
324
+ # ------------------------------------------------------------------
325
+ poly_vals: dict[str, float] = {"poly_alkyl": 0.0, "poly_aryl": 0.0, "poly_cyclic": 0.0}
326
+ _poly_key = {35: "poly_alkyl", 38: "poly_aryl", 45: "poly_cyclic"}
327
+ for match in emb_chain.matches:
328
+ if not match.is_group:
329
+ continue
330
+ key = _poly_key.get(match.group_id)
331
+ if key is not None:
332
+ poly_vals[key] += 1.0
333
+
334
+ # ------------------------------------------------------------------
335
+ # Build per_halogen_sizes (15 entries: 3 groups × 5 halogens)
336
+ # ------------------------------------------------------------------
337
+ phs: dict[str, float] = {
338
+ f"g{gid}_{h}": 0.0
339
+ for gid in PER_GROUP_IDS
340
+ for h in HALOGENS_ORDER
341
+ }
342
+ for match in emb_chain.matches:
343
+ if not match.is_group:
344
+ continue
345
+ row = _PER_GID_TO_ROW.get(match.group_id)
346
+ if row is None:
347
+ continue
348
+ gid = PER_GROUP_IDS[row]
349
+ h_val = match.get("halogen")
350
+ hal_hits: list[str]
351
+ if isinstance(h_val, list):
352
+ hal_hits = [x for x in h_val if x in _HAL_TO_COL]
353
+ elif h_val in _HAL_TO_COL:
354
+ hal_hits = [h_val]
355
+ else:
356
+ continue
357
+ max_size = 0.0
358
+ for comp in match.components:
359
+ s = comp.data.get("size", 0) or 0
360
+ if s > max_size:
361
+ max_size = float(s)
362
+ for hal in hal_hits:
363
+ k = f"g{gid}_{hal}"
364
+ if max_size > phs[k]:
365
+ phs[k] = max_size
366
+
367
+ # ------------------------------------------------------------------
368
+ # Build h_chain_sizes (convenience subset: H column of per_halogen_sizes)
369
+ # ------------------------------------------------------------------
370
+ h_chain: dict[str, float] = {
371
+ "alkyl_H": phs["g34_H"],
372
+ "aryl_H": phs["g37_H"],
373
+ "cyclic_H": phs["g44_H"],
374
+ }
375
+
376
+ # ------------------------------------------------------------------
377
+ # Build generic_groups (48 entries, ids 29-76)
378
+ # ------------------------------------------------------------------
379
+ gen: dict[str, float] = {f"g{gid}": 0.0 for gid in GENERIC_GROUP_VOCAB}
380
+ for match in emb_wc.matches:
381
+ if match.get("halogen") != "*":
382
+ continue
383
+ gid = match.group_id
384
+ k = f"g{gid}"
385
+ if k in gen:
386
+ gen[k] += 1.0
387
+
388
+ return GroupFeatureResult(
389
+ poly_counts=poly_vals,
390
+ per_halogen_sizes=phs,
391
+ h_chain_sizes=h_chain,
392
+ generic_groups=gen,
393
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PFASGroups
3
- Version: 3.4.0
3
+ Version: 3.4.2
4
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
5
  Author: Luc T. Miaz
6
6
  Author-email: "Luc T. Miaz" <luc@miaz.ch>
@@ -72,10 +72,48 @@ PFASGroups combines SMARTS pattern matching, molecular formula constraints, and
72
72
  ### Additional Tools
73
73
  - **Homologue Series Generation**: Iterative component shortening to explore theoretical chemical space
74
74
  - **Fingerprint Generation**: PFAS fingerprints for machine learning applications
75
+ - **Group Feature Extraction**: Fixed-length named feature vectors for ML pipelines (see below)
75
76
  - **Visualization**: Assign and visualize PFAS groupings
76
77
  - **Multiple Interfaces**: Python API, command-line tool, and browser-based JavaScript version (RDKitJS)
77
78
  - **Batch Processing**: Efficient analysis of large chemical databases
78
79
 
80
+ ## Group Feature Extraction
81
+
82
+ `extract_group_features` returns a structured `GroupFeatureResult` covering three
83
+ chemically meaningful feature sets derived from the six component groups and the
84
+ 48 generic functional groups:
85
+
86
+ ```python
87
+ from rdkit import Chem
88
+ from PFASGroups import extract_group_features
89
+
90
+ mol = Chem.MolFromSmiles("FC(F)(F)C(F)(F)C(F)(F)C(=O)O") # PFOA-like
91
+ r = extract_group_features(mol)
92
+
93
+ # Polyhalogenated group match counts (groups 35/38/45, all halogens combined)
94
+ print(r.poly_counts)
95
+ # {'poly_alkyl': 1.0, 'poly_aryl': 0.0, 'poly_cyclic': 0.0}
96
+
97
+ # Perhalogenated group max component sizes per halogen (groups 34/37/44 × F/Cl/Br/I/H)
98
+ print(r.per_halogen_sizes['g34_F']) # longest perfluoroalkyl chain (carbons)
99
+ # 4.0
100
+
101
+ # Alkyl chain size via H pseudo-halogen (convenience view of per_halogen_sizes H column)
102
+ print(r.h_chain_sizes) # octane: {'alkyl_H': 6.0, 'aryl_H': 0.0, 'cyclic_H': 0.0}
103
+
104
+ # Generic functional groups (ids 29-76, wildcard)
105
+ print({k: v for k, v in r.generic_groups.items() if v})
106
+ # {'g42': 1.0, 'g48': 1.0} → carboxylic acid + fluoride
107
+
108
+ # Fixed-length float32 array of shape (66,) for ML use
109
+ arr = r.to_array()
110
+ names = r.feature_names() # matching labels
111
+ ```
112
+
113
+ SMILES strings are accepted directly in addition to `rdkit.Chem.Mol` objects.
114
+ See the [Group Feature Extraction](docs/group_features.rst) documentation page for
115
+ the full feature layout, group ID table, and worked examples.
116
+
79
117
  ## Installation
80
118
 
81
119
  The recommended installation method is via pip. RDKit must already be available in the target environment. It is recommended to use an environment manager (like Conda/Mamba, e.g [Miniforge](https://github.com/conda-forge/miniforge)) and install RDKit via
@@ -454,7 +492,7 @@ h_group = HalogenGroup(
454
492
  )
455
493
 
456
494
  # Parse using H mode
457
- results = parse_smiles(smiles, halogens='H', pfas_groups=[h_group], bycomponent=True)
495
+ results = parse_smiles(smiles, halogens='H', pfas_groups=[h_group])
458
496
 
459
497
  # Inspect H-component matches for the custom group
460
498
  h_matches = [m for m in results[0].matches if m.get('id') == 9990 and m.get('type') == 'HalogenGroup']
@@ -808,6 +846,9 @@ See [USER_GUIDE.md](USER_GUIDE.md) for comprehensive examples including:
808
846
  - Integration with pandas and scikit-learn
809
847
 
810
848
  ## Summary of changes by version
849
+ - **Version 3.4.2**: Fixed ambiguity with OPPT 2023.
850
+
851
+ - **Version 3.4.1**: Added generic embedding for non-halogenated specific compounds which includes wildcard-components functional groups and halogen component size. Fixed H components and added cyclic and aryl H components.
811
852
 
812
853
  - **Version 3.4.0**: Added H components (use in homologue series generation) and wildcard components to match generic functional groups (broader use than PFAS).
813
854
 
@@ -16,6 +16,7 @@ PFASGroups/generate_homologues.py
16
16
  PFASGroups/generate_mol.py
17
17
  PFASGroups/generate_paper_figures.py
18
18
  PFASGroups/getter.py
19
+ PFASGroups/group_features.py
19
20
  PFASGroups/homologue_series.py
20
21
  PFASGroups/parser.py
21
22
  PFASGroups/prioritise.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PFASGroups
3
- Version: 3.4.0
3
+ Version: 3.4.2
4
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
5
  Author: Luc T. Miaz
6
6
  Author-email: "Luc T. Miaz" <luc@miaz.ch>
@@ -72,10 +72,48 @@ PFASGroups combines SMARTS pattern matching, molecular formula constraints, and
72
72
  ### Additional Tools
73
73
  - **Homologue Series Generation**: Iterative component shortening to explore theoretical chemical space
74
74
  - **Fingerprint Generation**: PFAS fingerprints for machine learning applications
75
+ - **Group Feature Extraction**: Fixed-length named feature vectors for ML pipelines (see below)
75
76
  - **Visualization**: Assign and visualize PFAS groupings
76
77
  - **Multiple Interfaces**: Python API, command-line tool, and browser-based JavaScript version (RDKitJS)
77
78
  - **Batch Processing**: Efficient analysis of large chemical databases
78
79
 
80
+ ## Group Feature Extraction
81
+
82
+ `extract_group_features` returns a structured `GroupFeatureResult` covering three
83
+ chemically meaningful feature sets derived from the six component groups and the
84
+ 48 generic functional groups:
85
+
86
+ ```python
87
+ from rdkit import Chem
88
+ from PFASGroups import extract_group_features
89
+
90
+ mol = Chem.MolFromSmiles("FC(F)(F)C(F)(F)C(F)(F)C(=O)O") # PFOA-like
91
+ r = extract_group_features(mol)
92
+
93
+ # Polyhalogenated group match counts (groups 35/38/45, all halogens combined)
94
+ print(r.poly_counts)
95
+ # {'poly_alkyl': 1.0, 'poly_aryl': 0.0, 'poly_cyclic': 0.0}
96
+
97
+ # Perhalogenated group max component sizes per halogen (groups 34/37/44 × F/Cl/Br/I/H)
98
+ print(r.per_halogen_sizes['g34_F']) # longest perfluoroalkyl chain (carbons)
99
+ # 4.0
100
+
101
+ # Alkyl chain size via H pseudo-halogen (convenience view of per_halogen_sizes H column)
102
+ print(r.h_chain_sizes) # octane: {'alkyl_H': 6.0, 'aryl_H': 0.0, 'cyclic_H': 0.0}
103
+
104
+ # Generic functional groups (ids 29-76, wildcard)
105
+ print({k: v for k, v in r.generic_groups.items() if v})
106
+ # {'g42': 1.0, 'g48': 1.0} → carboxylic acid + fluoride
107
+
108
+ # Fixed-length float32 array of shape (66,) for ML use
109
+ arr = r.to_array()
110
+ names = r.feature_names() # matching labels
111
+ ```
112
+
113
+ SMILES strings are accepted directly in addition to `rdkit.Chem.Mol` objects.
114
+ See the [Group Feature Extraction](docs/group_features.rst) documentation page for
115
+ the full feature layout, group ID table, and worked examples.
116
+
79
117
  ## Installation
80
118
 
81
119
  The recommended installation method is via pip. RDKit must already be available in the target environment. It is recommended to use an environment manager (like Conda/Mamba, e.g [Miniforge](https://github.com/conda-forge/miniforge)) and install RDKit via
@@ -454,7 +492,7 @@ h_group = HalogenGroup(
454
492
  )
455
493
 
456
494
  # Parse using H mode
457
- results = parse_smiles(smiles, halogens='H', pfas_groups=[h_group], bycomponent=True)
495
+ results = parse_smiles(smiles, halogens='H', pfas_groups=[h_group])
458
496
 
459
497
  # Inspect H-component matches for the custom group
460
498
  h_matches = [m for m in results[0].matches if m.get('id') == 9990 and m.get('type') == 'HalogenGroup']
@@ -808,6 +846,9 @@ See [USER_GUIDE.md](USER_GUIDE.md) for comprehensive examples including:
808
846
  - Integration with pandas and scikit-learn
809
847
 
810
848
  ## Summary of changes by version
849
+ - **Version 3.4.2**: Fixed ambiguity with OPPT 2023.
850
+
851
+ - **Version 3.4.1**: Added generic embedding for non-halogenated specific compounds which includes wildcard-components functional groups and halogen component size. Fixed H components and added cyclic and aryl H components.
811
852
 
812
853
  - **Version 3.4.0**: Added H components (use in homologue series generation) and wildcard components to match generic functional groups (broader use than PFAS).
813
854
 
@@ -24,10 +24,48 @@ PFASGroups combines SMARTS pattern matching, molecular formula constraints, and
24
24
  ### Additional Tools
25
25
  - **Homologue Series Generation**: Iterative component shortening to explore theoretical chemical space
26
26
  - **Fingerprint Generation**: PFAS fingerprints for machine learning applications
27
+ - **Group Feature Extraction**: Fixed-length named feature vectors for ML pipelines (see below)
27
28
  - **Visualization**: Assign and visualize PFAS groupings
28
29
  - **Multiple Interfaces**: Python API, command-line tool, and browser-based JavaScript version (RDKitJS)
29
30
  - **Batch Processing**: Efficient analysis of large chemical databases
30
31
 
32
+ ## Group Feature Extraction
33
+
34
+ `extract_group_features` returns a structured `GroupFeatureResult` covering three
35
+ chemically meaningful feature sets derived from the six component groups and the
36
+ 48 generic functional groups:
37
+
38
+ ```python
39
+ from rdkit import Chem
40
+ from PFASGroups import extract_group_features
41
+
42
+ mol = Chem.MolFromSmiles("FC(F)(F)C(F)(F)C(F)(F)C(=O)O") # PFOA-like
43
+ r = extract_group_features(mol)
44
+
45
+ # Polyhalogenated group match counts (groups 35/38/45, all halogens combined)
46
+ print(r.poly_counts)
47
+ # {'poly_alkyl': 1.0, 'poly_aryl': 0.0, 'poly_cyclic': 0.0}
48
+
49
+ # Perhalogenated group max component sizes per halogen (groups 34/37/44 × F/Cl/Br/I/H)
50
+ print(r.per_halogen_sizes['g34_F']) # longest perfluoroalkyl chain (carbons)
51
+ # 4.0
52
+
53
+ # Alkyl chain size via H pseudo-halogen (convenience view of per_halogen_sizes H column)
54
+ print(r.h_chain_sizes) # octane: {'alkyl_H': 6.0, 'aryl_H': 0.0, 'cyclic_H': 0.0}
55
+
56
+ # Generic functional groups (ids 29-76, wildcard)
57
+ print({k: v for k, v in r.generic_groups.items() if v})
58
+ # {'g42': 1.0, 'g48': 1.0} → carboxylic acid + fluoride
59
+
60
+ # Fixed-length float32 array of shape (66,) for ML use
61
+ arr = r.to_array()
62
+ names = r.feature_names() # matching labels
63
+ ```
64
+
65
+ SMILES strings are accepted directly in addition to `rdkit.Chem.Mol` objects.
66
+ See the [Group Feature Extraction](docs/group_features.rst) documentation page for
67
+ the full feature layout, group ID table, and worked examples.
68
+
31
69
  ## Installation
32
70
 
33
71
  The recommended installation method is via pip. RDKit must already be available in the target environment. It is recommended to use an environment manager (like Conda/Mamba, e.g [Miniforge](https://github.com/conda-forge/miniforge)) and install RDKit via
@@ -406,7 +444,7 @@ h_group = HalogenGroup(
406
444
  )
407
445
 
408
446
  # Parse using H mode
409
- results = parse_smiles(smiles, halogens='H', pfas_groups=[h_group], bycomponent=True)
447
+ results = parse_smiles(smiles, halogens='H', pfas_groups=[h_group])
410
448
 
411
449
  # Inspect H-component matches for the custom group
412
450
  h_matches = [m for m in results[0].matches if m.get('id') == 9990 and m.get('type') == 'HalogenGroup']
@@ -760,6 +798,9 @@ See [USER_GUIDE.md](USER_GUIDE.md) for comprehensive examples including:
760
798
  - Integration with pandas and scikit-learn
761
799
 
762
800
  ## Summary of changes by version
801
+ - **Version 3.4.2**: Fixed ambiguity with OPPT 2023.
802
+
803
+ - **Version 3.4.1**: Added generic embedding for non-halogenated specific compounds which includes wildcard-components functional groups and halogen component size. Fixed H components and added cyclic and aryl H components.
763
804
 
764
805
  - **Version 3.4.0**: Added H components (use in homologue series generation) and wildcard components to match generic functional groups (broader use than PFAS).
765
806
 
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>PFASGroups GUI – Help</title>
6
+ <title>PFASGroups GUI - Help</title>
7
7
  <style>
8
8
  :root {
9
9
  --orange: #E15D0B;
@@ -83,25 +83,25 @@
83
83
 
84
84
  <h3>Data sources</h3>
85
85
  <ul>
86
- <li><b>File</b> – CSV, Excel (.xlsx/.xls), or SQLite (.db/.sqlite). Browse for the
86
+ <li><b>File</b> - CSV, Excel (.xlsx/.xls), or SQLite (.db/.sqlite). Browse for the
87
87
  file, choose a sheet (Excel) and the SMILES / name columns.</li>
88
- <li><b>Database</b> – SQLite, MariaDB, or PostgreSQL. Fill in the connection form
88
+ <li><b>Database</b> - SQLite, MariaDB, or PostgreSQL. Fill in the connection form
89
89
  (host, port, database name, user, password, table name).</li>
90
- <li><b>Manual</b> – Paste SMILES directly. One per line, or comma-separated with
90
+ <li><b>Manual</b> - Paste SMILES directly. One per line, or comma-separated with
91
91
  optional names (<code>Name: SMILES</code>).</li>
92
92
  </ul>
93
93
 
94
94
  <h3>Options</h3>
95
95
  <ul>
96
- <li><b>Halogens</b> – Which halogens to include in the analysis. Start with
96
+ <li><b>Halogens</b> - Which halogens to include in the analysis. Start with
97
97
  <code>F only</code> for classical PFAS; broaden to include Cl/Br/I for expanded
98
98
  screening.</li>
99
- <li><b>Saturation filter</b> – <code>Perf. (CF₂/CF₃)</code> restricts to
99
+ <li><b>Saturation filter</b> - <code>Perf. (CF₂/CF₃)</code> restricts to
100
100
  perfluorinated chains; <code>All fluorinated</code> includes partially
101
101
  fluorinated structures.</li>
102
- <li><b>Compute metrics</b> – Calculate graph-theoretic component metrics (needed
103
- for Tabs 4–6).</li>
104
- <li><b>Check definitions</b> – Additionally test each molecule against the PFAS
102
+ <li><b>Compute metrics</b> - Calculate graph-theoretic component metrics (needed
103
+ for Tabs 4-6).</li>
104
+ <li><b>Check definitions</b> - Additionally test each molecule against the PFAS
105
105
  definitions ticked below the option.</li>
106
106
  </ul>
107
107
 
@@ -123,9 +123,9 @@
123
123
 
124
124
  <h3>Controls</h3>
125
125
  <ul>
126
- <li><b>Search</b> – filter cards by molecule name.</li>
127
- <li><b>Show All / Hide All</b> – toggle structure visibility for all cards.</li>
128
- <li><b>Export CSV</b> – save a summary table (name, SMILES, matched groups,
126
+ <li><b>Search</b> - filter cards by molecule name.</li>
127
+ <li><b>Show All / Hide All</b> - toggle structure visibility for all cards.</li>
128
+ <li><b>Export CSV</b> - save a summary table (name, SMILES, matched groups,
129
129
  definition results) to a CSV file.</li>
130
130
  </ul>
131
131
  </div>
@@ -133,24 +133,24 @@
133
133
  <!-- ────────────────────────────────────────────────────────────────── Tab 3 -->
134
134
  <h2><span class="step-num">3</span> Definition &amp; SMARTS Tester Tab</h2>
135
135
  <div class="card">
136
- <h3>Sub-tab A – PFAS Definitions</h3>
136
+ <h3>Sub-tab A - PFAS Definitions</h3>
137
137
  <p>Enter a SMILES string and test it against:</p>
138
138
  <ul>
139
139
  <li><b>Built-in definitions</b> (default, when the custom JSON field is left blank).</li>
140
- <li><b>Custom <code>PFASDefinition</code> JSON</b> – paste an array of definition
140
+ <li><b>Custom <code>PFASDefinition</code> JSON</b> - paste an array of definition
141
141
  objects. Each must have <code>id</code>, <code>name</code>,
142
142
  <code>smarts</code> (list of SMARTS strings), and optionally
143
143
  <code>fluorineRatio</code> and <code>description</code>.</li>
144
144
  </ul>
145
145
 
146
- <h3>Sub-tab B – Custom Group (SMARTS)</h3>
146
+ <h3>Sub-tab B - Custom Group (SMARTS)</h3>
147
147
  <p>Define a custom <code>HalogenGroup</code> and test a molecule against it. Fields:</p>
148
148
  <ul>
149
- <li><b>componentSmarts</b> – name of the underlying fluorine component pattern
149
+ <li><b>componentSmarts</b> - name of the underlying fluorine component pattern
150
150
  (e.g. <code>Perfluoroalkyl</code>).</li>
151
- <li><b>SMARTS dict (JSON)</b> – mapping of SMARTS pattern → required minimum count,
151
+ <li><b>SMARTS dict (JSON)</b> - mapping of SMARTS pattern → required minimum count,
152
152
  e.g. <code>{"[CX3](=O)[OX2H1]": 1}</code>.</li>
153
- <li><b>Constraints (JSON)</b> – element-count constraints,
153
+ <li><b>Constraints (JSON)</b> - element-count constraints,
154
154
  e.g. <code>{"gte": {"F": 4}}</code>.</li>
155
155
  </ul>
156
156
  <p>Diagnostics are printed as JSON in the right panel, showing which stage passed
@@ -166,11 +166,11 @@
166
166
 
167
167
  <h3>Modes</h3>
168
168
  <ul>
169
- <li><b>Intrinsic: total component size</b> – sum of all matched fragment sizes.</li>
170
- <li><b>Intrinsic: max component size</b> – largest single matched fragment.</li>
171
- <li><b>Intrinsic: total + max</b> – weighted combination;
169
+ <li><b>Intrinsic: total component size</b> - sum of all matched fragment sizes.</li>
170
+ <li><b>Intrinsic: max component size</b> - largest single matched fragment.</li>
171
+ <li><b>Intrinsic: total + max</b> - weighted combination;
172
172
  use α and β to balance the two terms.</li>
173
- <li><b>Reference-based KL divergence</b> – divergence from a supplied reference
173
+ <li><b>Reference-based KL divergence</b> - divergence from a supplied reference
174
174
  dataset. Supply a CSV/Excel/SQLite file as the reference.</li>
175
175
  </ul>
176
176
 
@@ -188,10 +188,10 @@
188
188
 
189
189
  <h3>Methods</h3>
190
190
  <ul>
191
- <li><b>UMAP</b> – fast, topology-preserving. Adjust <em>n_neighbours</em>
191
+ <li><b>UMAP</b> - fast, topology-preserving. Adjust <em>n_neighbours</em>
192
192
  (local structure) and <em>min_dist</em> (cluster compactness).</li>
193
- <li><b>PCA</b> – linear; no hyper-parameters.</li>
194
- <li><b>t-SNE</b> – non-linear; adjust <em>perplexity</em>.</li>
193
+ <li><b>PCA</b> - linear; no hyper-parameters.</li>
194
+ <li><b>t-SNE</b> - non-linear; adjust <em>perplexity</em>.</li>
195
195
  </ul>
196
196
 
197
197
  <h3>Fingerprint presets</h3>
@@ -218,13 +218,13 @@
218
218
 
219
219
  <h3>Fingerprint sets</h3>
220
220
  <ul>
221
- <li><b>PFASGroups presets</b> – any combination of the benchmark-validated
221
+ <li><b>PFASGroups presets</b> - any combination of the benchmark-validated
222
222
  fingerprint presets (binary, best, best_2, …).</li>
223
- <li><b>Morgan (RDKit)</b> – radius-2 Morgan fingerprint, 512 bits. Useful
223
+ <li><b>Morgan (RDKit)</b> - radius-2 Morgan fingerprint, 512 bits. Useful
224
224
  baseline.</li>
225
- <li><b>ToxPrint (729 bits)</b> – requires pyCSRML to be installed.</li>
226
- <li><b>TxP_PFAS (129 bits)</b> – PFAS-specific ToxPrint subset; requires pyCSRML.</li>
227
- <li><b>Custom TSV/CSV</b> – rows = molecules (same order as input), columns = bits.
225
+ <li><b>ToxPrint (729 bits)</b> - requires pyCSRML to be installed.</li>
226
+ <li><b>TxP_PFAS (129 bits)</b> - PFAS-specific ToxPrint subset; requires pyCSRML.</li>
227
+ <li><b>Custom TSV/CSV</b> - rows = molecules (same order as input), columns = bits.
228
228
  No header row.</li>
229
229
  </ul>
230
230
 
@@ -232,9 +232,9 @@
232
232
  <p>All fingerprint-set pairs are compared using the Bayesian correlated t-test
233
233
  (Benavoli et al. 2017) with ROPE = 0.01 ROC-AUC. The table reports:</p>
234
234
  <ul>
235
- <li><b>P(A&gt;B)</b> – probability that set A is meaningfully better than B.</li>
236
- <li><b>P(ROPE)</b> – probability that the two sets are practically equivalent.</li>
237
- <li><b>P(B&gt;A)</b> – probability that set B is meaningfully better than A.</li>
235
+ <li><b>P(A&gt;B)</b> - probability that set A is meaningfully better than B.</li>
236
+ <li><b>P(ROPE)</b> - probability that the two sets are practically equivalent.</li>
237
+ <li><b>P(B&gt;A)</b> - probability that set B is meaningfully better than A.</li>
238
238
  </ul>
239
239
 
240
240
  <div class="note">
@@ -247,8 +247,8 @@
247
247
  <h2>Keyboard shortcuts</h2>
248
248
  <div class="card">
249
249
  <ul>
250
- <li><code>Ctrl+Q</code> – quit the application.</li>
251
- <li><code>Ctrl+W</code> – close (same as Ctrl+Q on Windows).</li>
250
+ <li><code>Ctrl+Q</code> - quit the application.</li>
251
+ <li><code>Ctrl+W</code> - close (same as Ctrl+Q on Windows).</li>
252
252
  </ul>
253
253
  </div>
254
254
 
@@ -266,9 +266,9 @@
266
266
  <div class="card">
267
267
  <ul>
268
268
  <li>Benavoli A et al. (2017) <em>Time for a Change: a Tutorial for Comparing
269
- Multiple Classifiers Through Bayesian Analysis.</em> JMLR 18(1):2653–2688.</li>
269
+ Multiple Classifiers Through Bayesian Analysis.</em> JMLR 18(1):2653-2688.</li>
270
270
  <li>Nadeau C &amp; Bengio Y (2003) <em>Inference for the Generalization Error.</em>
271
- Machine Learning 52(3):239–281.</li>
271
+ Machine Learning 52(3):239-281.</li>
272
272
  </ul>
273
273
  </div>
274
274
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "PFASGroups"
7
- version = "3.4.0"
7
+ version = "3.4.2"
8
8
  description = "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."
9
9
  authors = [
10
10
  { name = "Luc T. Miaz", email = "luc@miaz.ch" }
@@ -1,12 +1,12 @@
1
1
  [metadata]
2
2
  name = PFASGroups
3
- version = 3.3.0
3
+ version = 3.4.2
4
4
  description = A comprehensive cheminformatics package for automated detection, classification, and analysis of halogenated substances, with a focus on per- and polyfluoroalkyl substances (PFAS). Supports multi-halogen fingerprinting (F, Cl, Br, I) with per/poly saturation filtering and stackable fingerprint vectors.
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
7
7
  author = Luc T. Miaz
8
8
  author_email = luc@miaz.ch
9
- license = MIT
9
+ license = CC-BY-NC-4.0
10
10
 
11
11
  [egg_info]
12
12
  tag_build =
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes