PFASGroups 3.2.2__tar.gz → 3.4.0__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 (81) hide show
  1. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/HalogenGroupModel.py +7 -8
  2. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/PFASEmbeddings.py +15 -7
  3. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/__init__.py +2 -2
  4. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/cli.py +2 -2
  5. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/core.py +5 -4
  6. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/data/Halogen_groups_smarts.json +34 -6
  7. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/data/component_smarts_halogens.json +9 -0
  8. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/generate_homologues.py +27 -14
  9. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/generate_mol.py +1 -1
  10. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/getter.py +1 -1
  11. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/homologue_series.py +8 -8
  12. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/parser.py +234 -9
  13. {pfasgroups-3.2.2 → pfasgroups-3.4.0/PFASGroups.egg-info}/PKG-INFO +229 -7
  14. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups.egg-info/SOURCES.txt +22 -0
  15. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups.egg-info/entry_points.txt +1 -0
  16. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups.egg-info/requires.txt +8 -0
  17. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups.egg-info/top_level.txt +1 -0
  18. {pfasgroups-3.2.2/PFASGroups.egg-info → pfasgroups-3.4.0}/PKG-INFO +229 -7
  19. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/README.md +221 -6
  20. pfasgroups-3.4.0/gui/__init__.py +1 -0
  21. pfasgroups-3.4.0/gui/__main__.py +21 -0
  22. pfasgroups-3.4.0/gui/app.py +162 -0
  23. pfasgroups-3.4.0/gui/data/help.html +276 -0
  24. pfasgroups-3.4.0/gui/style.py +407 -0
  25. pfasgroups-3.4.0/gui/tabs/__init__.py +1 -0
  26. pfasgroups-3.4.0/gui/tabs/chemspace_tab.py +250 -0
  27. pfasgroups-3.4.0/gui/tabs/classify_tab.py +823 -0
  28. pfasgroups-3.4.0/gui/tabs/definition_tester_tab.py +334 -0
  29. pfasgroups-3.4.0/gui/tabs/help_tab.py +97 -0
  30. pfasgroups-3.4.0/gui/tabs/modelling_tab.py +489 -0
  31. pfasgroups-3.4.0/gui/tabs/prioritise_tab.py +303 -0
  32. pfasgroups-3.4.0/gui/tabs/results_tab.py +350 -0
  33. pfasgroups-3.4.0/gui/utils/__init__.py +1 -0
  34. pfasgroups-3.4.0/gui/utils/chemspace.py +170 -0
  35. pfasgroups-3.4.0/gui/utils/export_dialog.py +742 -0
  36. pfasgroups-3.4.0/gui/utils/fingerprints.py +91 -0
  37. pfasgroups-3.4.0/gui/utils/io_readers.py +198 -0
  38. pfasgroups-3.4.0/gui/utils/modelling.py +176 -0
  39. pfasgroups-3.4.0/gui/utils/mol_renderer.py +69 -0
  40. pfasgroups-3.4.0/gui/workers.py +362 -0
  41. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/pyproject.toml +12 -2
  42. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/setup.cfg +1 -1
  43. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_bde_resistance.py +0 -1
  44. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_fingerprints_vs_txppfas.py +4 -4
  45. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_generate_homologues.py +31 -7
  46. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_n_spacer_ring_size.py +15 -15
  47. pfasgroups-3.4.0/tests/test_parser_wildcard.py +188 -0
  48. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_prioritise.py +19 -7
  49. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_readme_and_docs_examples.py +1 -1
  50. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/HalogenGroups/__init__.py +0 -0
  51. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/ComponentsSolverModel.py +0 -0
  52. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/PFASDefinitionModel.py +0 -0
  53. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/data/PFAS_definitions_smarts.json +0 -0
  54. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/data/component_smarts.json +0 -0
  55. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/data/diatomic_bonds_dict.json +0 -0
  56. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/draw_mols.py +0 -0
  57. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/embeddings.py +0 -0
  58. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/fragmentation.py +0 -0
  59. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/generate_paper_figures.py +0 -0
  60. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups/prioritise.py +0 -0
  61. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/PFASGroups.egg-info/dependency_links.txt +0 -0
  62. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_component_fractions.py +0 -0
  63. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_component_ratios.py +0 -0
  64. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_comprehensive_metrics.py +0 -0
  65. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_database_integration.py +0 -0
  66. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_definition_comparison.py +0 -0
  67. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_fraction_analysis.py +0 -0
  68. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_halogen_groups_smarts.py +0 -0
  69. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_linker_smarts.py +0 -0
  70. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_metrics.py +0 -0
  71. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_metrics_detailed.py +0 -0
  72. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_pfasstructv5.py +0 -0
  73. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_results_fingerprint.py +0 -0
  74. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_results_model.py +0 -0
  75. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_results_sql.py +0 -0
  76. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_run_groups_definitions_tests.py +0 -0
  77. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_smarts_atom_count.py +0 -0
  78. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_smarts_matching.py +0 -0
  79. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_smarts_precompute.py +0 -0
  80. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_summary.py +0 -0
  81. {pfasgroups-3.2.2 → pfasgroups-3.4.0}/tests/test_telomer_validation.py +0 -0
@@ -154,11 +154,11 @@ class HalogenGroup():
154
154
  # No metadata available to infer component SMARTS
155
155
  return
156
156
  # prepare halogens (accepts list, str or None)
157
- if isinstance(self.componentHalogens,list) and len(set(self.componentHalogens).intersection(['F','Cl','Br','I'])) == 0:
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
159
  if self.componentHalogens is None:
160
160
  self.componentHalogens = ['F','Cl','Br','I']
161
- elif isinstance(self.componentHalogens, str) and self.componentHalogens in ['F','Cl','Br','I']:
161
+ elif isinstance(self.componentHalogens, str) and self.componentHalogens in ['F','Cl','Br','I','H']:
162
162
  self.componentHalogens = [self.componentHalogens]
163
163
  if self.componentSaturation is None:
164
164
  self.componentSaturation = ['per','poly']
@@ -226,9 +226,9 @@ class HalogenGroup():
226
226
  constraints : dict
227
227
  Component constraints with optional keys:
228
228
 
229
- * ``'gte'`` – ``{element: min_count}``; component must have at least
229
+ * ``'gte'`` - ``{element: min_count}``; component must have at least
230
230
  *min_count* atoms of *element*.
231
- * ``'exclude'`` – list of element symbols that must be absent from
231
+ * ``'exclude'`` - list of element symbols that must be absent from
232
232
  the component.
233
233
 
234
234
  Returns
@@ -578,13 +578,12 @@ class HalogenGroup():
578
578
  if len(augmented_matched_components) == 0:
579
579
  return 0, [], 0, []
580
580
 
581
- # Get all component sizes from all path types
582
- all_components = list(set([comp for comps in augmented_matched_components for comp in comps]))
583
- component_sizes = [len(x) for x in all_components]
581
+ # Get all component sizes from matched component dicts
582
+ component_sizes = [comp.get('size', 0) for comp in augmented_matched_components]
584
583
 
585
584
  self.all_matches = [] # Clear matches after use
586
585
  self.component_specific_extra_atoms = []
587
- return max([0] + component_sizes), component_sizes, len(all_components), augmented_matched_components
586
+ return max([0] + component_sizes), component_sizes, len(augmented_matched_components), augmented_matched_components
588
587
 
589
588
  def find_aryl_components(self,mol, component_solver=None, **kwargs):
590
589
  """Find aryl components in a molecule with comprehensive metrics."""
@@ -198,7 +198,7 @@ def _ansi(text: str, *codes: str) -> str:
198
198
 
199
199
 
200
200
  # ---------------------------------------------------------------------------
201
- # Molecule-highlight colour palettes (RGB float triples, 0–1)
201
+ # Molecule-highlight colour palettes (RGB float triples, 0-1)
202
202
  # ---------------------------------------------------------------------------
203
203
 
204
204
  # Highlight colours by halogen element — mapped to the project colour palette
@@ -311,7 +311,7 @@ def _get_group_info() -> Dict[int, Dict[str, str]]:
311
311
 
312
312
  # Groups always excluded from the non-OECD category label (perhalogenated /
313
313
  # polyhalogenated alkyl catch-alls that add no structural specificity).
314
- _CLASSIFY_EXCLUDED_IDS: frozenset = frozenset({51, 52})
314
+ _CLASSIFY_EXCLUDED_IDS: frozenset = frozenset({34,35}) # "Perhalogenated alkyl" and "Polyhalogenated alkyl"
315
315
 
316
316
  # Name subsumption for non-OECD classification: when a more-specific group
317
317
  # name (key) is present, every name in its list is suppressed.
@@ -642,11 +642,19 @@ class MatchView(dict):
642
642
 
643
643
  @property
644
644
  def group_id(self) -> Optional[int]:
645
- return self.get("id") if self.is_group else None
645
+ # Keep legacy semantics for HalogenGroup while also exposing wildcard
646
+ # ids through the same convenience accessor.
647
+ if self.is_group or self.get("type") == "WildcardGroup":
648
+ return self.get("id")
649
+ return None
646
650
 
647
651
  @property
648
652
  def group_name(self) -> Optional[str]:
649
- return self.get("group_name") if self.is_group else None
653
+ # Keep legacy semantics for HalogenGroup while also exposing wildcard
654
+ # group names through the same convenience accessor.
655
+ if self.is_group or self.get("type") == "WildcardGroup":
656
+ return self.get("group_name")
657
+ return None
650
658
 
651
659
  @property
652
660
  def components(self) -> List[ComponentView]:
@@ -2798,11 +2806,11 @@ class PFASEmbeddingSet(list):
2798
2806
  Parameters
2799
2807
  ----------
2800
2808
  color_by : None | ``'top_group'`` | list of str
2801
- * ``None`` – return ``(None, None, None)``; callers fall back to a
2809
+ * ``None`` - return ``(None, None, None)``; callers fall back to a
2802
2810
  single default colour.
2803
- * ``'top_group'`` – derive one label per molecule from the
2811
+ * ``'top_group'`` - derive one label per molecule from the
2804
2812
  highest ``match_count`` ``HalogenGroup`` match.
2805
- * list of str – use directly as per-molecule labels (must be the
2813
+ * list of str - use directly as per-molecule labels (must be the
2806
2814
  same length as ``self``).
2807
2815
  """
2808
2816
  import matplotlib.pyplot as plt
@@ -3,7 +3,7 @@ from .HalogenGroupModel import HalogenGroup
3
3
  from .PFASDefinitionModel import PFASDefinition
4
4
  from .ComponentsSolverModel import ComponentsSolver
5
5
  from .core import rdkit_disable_log, HALOGEN_GROUPS_FILE
6
- from .parser import parse_smiles, parse_mols, parse_mol, parse_groups_in_mol, parse_from_database, setup_halogen_groups_database, load_HalogenGroups
6
+ from .parser import parse_smiles, parse_mols, parse_mol, parse_groups_in_mol, parse_from_database, setup_halogen_groups_database, load_HalogenGroups, _match_wildcard_groups_in_mol
7
7
  # PFASFingerprint: convenience alias for parse_smiles — returns a PFASEmbeddingSet
8
8
  PFASFingerprint = parse_smiles
9
9
  from .draw_mols import plot_mol, plot_mols, plot_HalogenGroups
@@ -16,6 +16,6 @@ from .fragmentation import generate_degradation_products
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
18
  __version__ = "3.2.0"
19
- __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"]
19
+ __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
20
  __all__.extend(['PFASEmbedding', 'PFASEmbeddingSet', 'EmbeddingArray', 'ResultsModel', 'MoleculeResult', 'prioritise_molecules', 'prioritize_molecules', 'get_priority_statistics'])
21
21
  __all__.extend(['HomologueSeries', 'HomologueEntry'])
@@ -47,7 +47,7 @@ Examples:
47
47
  # List available path types
48
48
  PFASGroups list-paths
49
49
 
50
- Note: Use get_componentSMARTSs() and get_PFASGroups() in Python to extend defaults.
50
+ Note: Use get_componentSMARTSs() and get_HalogenGroups() in Python to extend defaults.
51
51
  """
52
52
  )
53
53
 
@@ -186,7 +186,7 @@ Note: Use get_componentSMARTSs() and get_PFASGroups() in Python to extend defaul
186
186
  # List groups command
187
187
  list_parser = subparsers.add_parser(
188
188
  'list-groups',
189
- help='List available PFAS groups (use in Python to extend with get_PFASGroups)'
189
+ help='List available PFAS groups (use in Python to extend with get_HalogenGroups)'
190
190
  )
191
191
  list_parser.add_argument(
192
192
  '-o', '--output',
@@ -204,11 +204,11 @@ def fragment_until_valence_is_correct(mol, frags, verbose=False, _events=None):
204
204
  When *True* the function returns a 2-tuple ``(frags, events)`` instead
205
205
  of just ``frags``. *events* is a list of dicts, one per fragmentation
206
206
  step, each containing:
207
- ``atom_idx`` – index of the offending atom,
208
- ``error`` – the original :class:`~rdkit.Chem.AtomValenceException`
207
+ ``atom_idx`` - index of the offending atom,
208
+ ``error`` - the original :class:`~rdkit.Chem.AtomValenceException`
209
209
  message,
210
- ``n_fragments`` – how many pieces the bond-cut produced,
211
- ``smiles`` – SMILES of the fragment being cut (may be ``None`` if
210
+ ``n_fragments`` - how many pieces the bond-cut produced,
211
+ ``smiles`` - SMILES of the fragment being cut (may be ``None`` if
212
212
  RDKit cannot generate one for an unsanitized mol).
213
213
  _events : list or None
214
214
  Internal accumulator used during recursion; do **not** pass explicitly.
@@ -370,6 +370,7 @@ def add_componentSmarts(filename = COMPONENTS_FILE):
370
370
  # Normalize filters to lists
371
371
  if halogens is not None:
372
372
  halogens = [halogens] if isinstance(halogens, str) else list(halogens)
373
+ halogens = [h for h in halogens if h != '*']
373
374
  if form is not None:
374
375
  form = [form] if isinstance(form, str) else list(form)
375
376
  if saturation is not None:
@@ -4,6 +4,7 @@
4
4
  "name": "OECD perfluoroalkyl alcohols",
5
5
  "alias": "Perfluoroalkyl alcohols",
6
6
  "componentSmarts": "Perfluoroalkyl",
7
+ "componentHalogen": "F",
7
8
  "linker_smarts": null,
8
9
  "constraints": {
9
10
  "only": [
@@ -57,6 +58,7 @@
57
58
  "name": "OECD telomer alcohols",
58
59
  "alias": "telomer alcohols",
59
60
  "componentSmarts": "Perfluoroalkyl",
61
+ "componentHalogen": "F",
60
62
  "linker_smarts": null,
61
63
  "max_dist_from_comp": 0,
62
64
  "constraints": {
@@ -132,6 +134,7 @@
132
134
  "name": "OECD perfluoroalkane",
133
135
  "alias": "Perfluoroalkane",
134
136
  "componentSmarts": "Perfluoroalkyl",
137
+ "componentHalogen": "F",
135
138
  "linker_smarts": null,
136
139
  "constraints": {
137
140
  "only": [
@@ -181,6 +184,7 @@
181
184
  "name": "OECD semi-fluorinated alkanes",
182
185
  "alias": "SFAs",
183
186
  "componentSmarts": "Polyfluoroalkyl",
187
+ "componentHalogen": "F",
184
188
  "linker_smarts": null,
185
189
  "constraints": {
186
190
  "gte": {
@@ -236,6 +240,7 @@
236
240
  "name": "OECD perfluoroalkene",
237
241
  "alias": "Perfluoroalkene",
238
242
  "componentSmarts": null,
243
+ "componentHalogen": "F",
239
244
  "max_dist_from_comp": 16,
240
245
  "linker_smarts": null,
241
246
  "constraints": {
@@ -287,6 +292,7 @@
287
292
  "name": "OECD perfluoroalkyl carboxylic acids",
288
293
  "alias": "PFCAs",
289
294
  "componentSmarts": "Perfluoroalkyl",
295
+ "componentHalogen": "F",
290
296
  "linker_smarts": null,
291
297
  "constraints": {
292
298
  "eq": {
@@ -343,6 +349,7 @@
343
349
  "name": "OECD polyfluoroalkyl carboxylic acid",
344
350
  "alias": "PolyFCAs",
345
351
  "componentSmarts": "Polyfluoroalkyl",
352
+ "componentHalogen": "F",
346
353
  "linker_smarts": null,
347
354
  "constraints": {
348
355
  "eq": {
@@ -404,6 +411,7 @@
404
411
  "name": "OECD perfluoroalkyl dicarboxylic acids",
405
412
  "alias": "PFdiCAs",
406
413
  "componentSmarts": "Perfluoroalkyl",
414
+ "componentHalogen": "F",
407
415
  "max_dist_from_comp": 1,
408
416
  "linker_smarts": null,
409
417
  "constraints": {
@@ -462,6 +470,7 @@
462
470
  "name": "OECD perfluoroalkylether carboxylic acids",
463
471
  "alias": "PFECAs",
464
472
  "componentSmarts": "Perfluoroalkyl",
473
+ "componentHalogen": "F",
465
474
  "linker_smarts": null,
466
475
  "constraints": {
467
476
  "gte": {
@@ -518,6 +527,7 @@
518
527
  "name": "OECD polyfluoroalkylether carboxylic acid",
519
528
  "alias": "PolyFECAs",
520
529
  "componentSmarts": "Polyfluoroalkyl",
530
+ "componentHalogen": "F",
521
531
  "linker_smarts": null,
522
532
  "constraints": {
523
533
  "gte": {
@@ -582,6 +592,7 @@
582
592
  "name": "OECD hydrofluoroethers",
583
593
  "alias": "HFEs",
584
594
  "componentSmarts": "Polyfluoroalkyl",
595
+ "componentHalogen": "F",
585
596
  "linker_smarts": null,
586
597
  "constraints": {
587
598
  "gte": {
@@ -637,6 +648,7 @@
637
648
  "name": "OECD hydrofluorocarbons",
638
649
  "alias": "HFCs",
639
650
  "componentSmarts": "Polyfluoroalkyl",
651
+ "componentHalogen": "F",
640
652
  "linker_smarts": null,
641
653
  "constraints": {
642
654
  "gte": {
@@ -689,6 +701,7 @@
689
701
  "name": "OECD perfluoroalkyl iodides",
690
702
  "alias": "PFAIs",
691
703
  "componentSmarts": "Perfluoroalkyl",
704
+ "componentHalogen": "F",
692
705
  "linker_smarts": null,
693
706
  "constraints": {
694
707
  "gte": {
@@ -735,6 +748,7 @@
735
748
  "name": "OECD perfluoroalkyl ketones",
736
749
  "alias": "Perfluoroalkyl ketones",
737
750
  "componentSmarts": "Perfluoroalkyl",
751
+ "componentHalogen": "F",
738
752
  "linker_smarts": null,
739
753
  "constraints": {
740
754
  "only": [
@@ -787,6 +801,7 @@
787
801
  "name": "OECD semi-fluoroalkyl ketones",
788
802
  "alias": "Semi-fluoroalkyl ketones",
789
803
  "componentSmarts": "Polyfluoroalkyl",
804
+ "componentHalogen": "F",
790
805
  "linker_smarts": null,
791
806
  "constraints": {
792
807
  "gte": {
@@ -842,6 +857,7 @@
842
857
  "name": "OECD hydrofluoroolefins",
843
858
  "alias": "HFOs",
844
859
  "componentSmarts": "Polyfluoroalkyl",
860
+ "componentHalogen": "F",
845
861
  "max_dist_from_comp": 16,
846
862
  "linker_smarts": "[CH2]",
847
863
  "constraints": {
@@ -895,6 +911,7 @@
895
911
  "name": "OECD perfluoropolyethers",
896
912
  "alias": "PFPEs",
897
913
  "componentSmarts": "Perfluoroalkyl",
914
+ "componentHalogen": "F",
898
915
  "linker_smarts": null,
899
916
  "constraints": {
900
917
  "only": [
@@ -948,6 +965,7 @@
948
965
  "name": "OECD side-chain fluorinated aromatics",
949
966
  "alias": "Side-chain fluorinated aromatics",
950
967
  "componentSmarts": "Perfluoroalkyl",
968
+ "componentHalogen": "F",
951
969
  "linker_smarts": null,
952
970
  "constraints": {
953
971
  "gte": {
@@ -985,6 +1003,7 @@
985
1003
  "name": "OECD perfluoroalkylether sulfonic acids",
986
1004
  "alias": "PFESAs",
987
1005
  "componentSmarts": "Perfluoroalkyl",
1006
+ "componentHalogen": "F",
988
1007
  "linker_smarts": null,
989
1008
  "constraints": {
990
1009
  "eq": {
@@ -1049,6 +1068,7 @@
1049
1068
  "name": "OECD polyfluoroalkylether sulfonic acid",
1050
1069
  "alias": "PolyFESAs",
1051
1070
  "componentSmarts": "Polyfluoroalkyl",
1071
+ "componentHalogen": "F",
1052
1072
  "linker_smarts": null,
1053
1073
  "constraints": {
1054
1074
  "eq": {
@@ -1121,6 +1141,7 @@
1121
1141
  "name": "OECD perfluoroalkyl sulfinic acids",
1122
1142
  "alias": "PFSiAs",
1123
1143
  "componentSmarts": "Perfluoroalkyl",
1144
+ "componentHalogen": "F",
1124
1145
  "linker_smarts": null,
1125
1146
  "constraints": {
1126
1147
  "eq": {
@@ -1178,6 +1199,7 @@
1178
1199
  "name": "OECD perfluoroalkyl sulfonic acids",
1179
1200
  "alias": "PFSAs",
1180
1201
  "componentSmarts": "Perfluoroalkyl",
1202
+ "componentHalogen": "F",
1181
1203
  "linker_smarts": null,
1182
1204
  "constraints": {
1183
1205
  "eq": {
@@ -1236,6 +1258,7 @@
1236
1258
  "name": "OECD polyfluoroalkyl sulfonic acid",
1237
1259
  "alias": "PolyFSAs",
1238
1260
  "componentSmarts": "Polyfluoroalkyl",
1261
+ "componentHalogen": "F",
1239
1262
  "linker_smarts": null,
1240
1263
  "constraints": {
1241
1264
  "eq": {
@@ -1302,6 +1325,7 @@
1302
1325
  "name": "OECD perfluoroalkyl disulfonic acids",
1303
1326
  "alias": "PFdiSAs",
1304
1327
  "componentSmarts": "Perfluoroalkyl",
1328
+ "componentHalogen": "F",
1305
1329
  "max_dist_from_comp": 1,
1306
1330
  "linker_smarts": null,
1307
1331
  "constraints": {
@@ -1364,6 +1388,7 @@
1364
1388
  "name": "OECD perfluoroalkane sulfonyl fluorides",
1365
1389
  "alias": "PASFs",
1366
1390
  "componentSmarts": "Perfluoroalkyl",
1391
+ "componentHalogen": "F",
1367
1392
  "max_dist_from_comp": 0,
1368
1393
  "linker_smarts": null,
1369
1394
  "constraints": {
@@ -1411,6 +1436,7 @@
1411
1436
  "name": "OECD perfluoroalkyl phosphinic acids",
1412
1437
  "alias": "PFPiAs",
1413
1438
  "componentSmarts": "Perfluoroalkyl",
1439
+ "componentHalogen": "F",
1414
1440
  "linker_smarts": null,
1415
1441
  "constraints": {
1416
1442
  "eq": {
@@ -1468,6 +1494,7 @@
1468
1494
  "name": "OECD perfluoroalkyl phosphonic acids",
1469
1495
  "alias": "PFPAs",
1470
1496
  "componentSmarts": "Perfluoroalkyl",
1497
+ "componentHalogen": "F",
1471
1498
  "linker_smarts": null,
1472
1499
  "constraints": {
1473
1500
  "eq": {
@@ -1525,6 +1552,7 @@
1525
1552
  "name": "OECD perfluoroalkyl-tert-amines",
1526
1553
  "alias": "Perfluoroalkyl-tert-amines",
1527
1554
  "componentSmarts": "Perfluoroalkyl",
1555
+ "componentHalogen": "F",
1528
1556
  "linker_smarts": null,
1529
1557
  "constraints": {
1530
1558
  "only": [
@@ -4258,8 +4286,8 @@
4258
4286
  },
4259
4287
  {
4260
4288
  "id": 65,
4261
- "name": "thia keto propanoic acid",
4262
- "alias": "Thia keto propanoic acid",
4289
+ "name": "thioester keto dicarboxylic acid",
4290
+ "alias": "Thioester keto dicarboxylic acid",
4263
4291
  "componentSmarts": null,
4264
4292
  "linker_smarts": null,
4265
4293
  "max_dist_from_comp": 0,
@@ -4331,7 +4359,7 @@
4331
4359
  "sorting": {
4332
4360
  "category": "generic",
4333
4361
  "elements": "SCO",
4334
- "group": "thia keto propanoic acids",
4362
+ "group": "thioester keto dicarboxylic acids",
4335
4363
  "saturation": null,
4336
4364
  "aggregated": false
4337
4365
  }
@@ -7871,8 +7899,8 @@
7871
7899
  },
7872
7900
  {
7873
7901
  "id": 106,
7874
- "name": "telomer thia keto propanoic acid",
7875
- "alias": "telomer thia keto propanoic acid",
7902
+ "name": "telomer thioester keto dicarboxylic acid",
7903
+ "alias": "telomer thioester keto dicarboxylic acid",
7876
7904
  "componentSmarts": null,
7877
7905
  "componentSaturation": "per",
7878
7906
  "linker_smarts": "[CH2X4]",
@@ -7960,7 +7988,7 @@
7960
7988
  "sorting": {
7961
7989
  "category": "telomer",
7962
7990
  "elements": "SCO",
7963
- "group": "thia keto propanoic acids",
7991
+ "group": "thioester keto dicarboxylic acids",
7964
7992
  "saturation": null,
7965
7993
  "aggregated": false
7966
7994
  }
@@ -138,5 +138,14 @@
138
138
  "component": "[#6aR$([#6aR][I,#6aR$([#6aR][#53])]),#6aR$([#6aR]([#9,#17,#35,#53])[#6aR]I)]"
139
139
  }
140
140
  }
141
+ },
142
+ "H": {
143
+ "alkyl": {
144
+ "per": {
145
+ "name": "Alkyl",
146
+ "component": "[CH2]",
147
+ "end": "[CH3]"
148
+ }
149
+ }
141
150
  }
142
151
  }
@@ -44,7 +44,8 @@ def _mol_from_input(mol_or_string):
44
44
  )
45
45
 
46
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}
47
+ # 'H' is included to support CH2-based hydrocarbon homologue series
48
+ _HALOGEN_ATOMIC_NUM = {'F': 9, 'Cl': 17, 'Br': 35, 'I': 53, 'H': 1}
48
49
 
49
50
  # Default componentSmartsName per halogen (per-halo, alkyl chain)
50
51
  _DEFAULT_COMPONENT_NAME = {
@@ -52,6 +53,7 @@ _DEFAULT_COMPONENT_NAME = {
52
53
  'Cl': 'Perchloroalkyl',
53
54
  'Br': 'Perbromoalkyl',
54
55
  'I': 'Periodoalkyl',
56
+ 'H': 'Alkyl',
55
57
  }
56
58
 
57
59
 
@@ -82,8 +84,8 @@ def find_halogenated_components(mol, component_smarts, halogen='F'):
82
84
  list of dict
83
85
  Each entry describes one halogenated component:
84
86
 
85
- - ``'component'`` (``frozenset[int]``) – atom indices of all component atoms.
86
- - ``'cx2_carbons'`` (``list[int]``) – backbone C atoms bearing ≥ 2 halogen
87
+ - ``'component'`` (``frozenset[int]``) - atom indices of all component atoms.
88
+ - ``'cx2_carbons'`` (``list[int]``) - backbone C atoms bearing ≥ 2 halogen
87
89
  substituents (i.e., candidate ``CX2`` units for homologue generation).
88
90
  """
89
91
  component_atom_idxs = get_substruct(mol, component_smarts)
@@ -103,14 +105,24 @@ def find_halogenated_components(mol, component_smarts, halogen='F'):
103
105
  # Identify CX2 backbone carbons: C in component bearing *exactly* 2 halogen
104
106
  # neighbours. Terminal CF3 (3 halogens) and mono-halo atoms are excluded —
105
107
  # 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
- ]
108
+ #
109
+ # For halogen='H', H atoms are implicit in RDKit molecules, so we use
110
+ # GetTotalNumHs() instead of iterating explicit neighbours.
111
+ if halogen == 'H':
112
+ cx2 = [
113
+ idx for idx in cc
114
+ if mol.GetAtomWithIdx(idx).GetSymbol() == 'C'
115
+ and mol.GetAtomWithIdx(idx).GetTotalNumHs() == 2
116
+ ]
117
+ else:
118
+ cx2 = [
119
+ idx for idx in cc
120
+ if mol.GetAtomWithIdx(idx).GetSymbol() == 'C'
121
+ and sum(
122
+ 1 for nb in mol.GetAtomWithIdx(idx).GetNeighbors()
123
+ if nb.GetSymbol() == halogen
124
+ ) == 2
125
+ ]
114
126
  results.append({'component': frozenset(cc), 'cx2_carbons': cx2})
115
127
 
116
128
  return results
@@ -155,7 +167,7 @@ def generate_homologues(mol_input, componentSmartsName=None, componentSmartss=No
155
167
  Returns
156
168
  -------
157
169
  dict
158
- ``{InChIKey: {formula: rdkit.Chem.Mol}}`` – all unique shorter
170
+ ``{InChIKey: {formula: rdkit.Chem.Mol}}`` - all unique shorter
159
171
  homologues, keyed first by InChIKey and then by molecular formula.
160
172
 
161
173
  Raises
@@ -169,7 +181,7 @@ def generate_homologues(mol_input, componentSmartsName=None, componentSmartss=No
169
181
  --------
170
182
  >>> from rdkit import Chem
171
183
  >>> from HalogenGroups.generate_homologues import generate_homologues
172
- >>> # PFOA – generate all shorter perfluoroalkyl chain homologues
184
+ >>> # PFOA - generate all shorter perfluoroalkyl chain homologues
173
185
  >>> pfoa = Chem.MolFromSmiles('OC(=O)' + 'C(F)(F)' * 7 + 'F')
174
186
  >>> homologues = generate_homologues(pfoa)
175
187
  >>> print(len(homologues)) # 6 (C2-C7)
@@ -198,7 +210,8 @@ def generate_homologues(mol_input, componentSmartsName=None, componentSmartss=No
198
210
 
199
211
  if halogen not in _HALOGEN_ATOMIC_NUM:
200
212
  raise ValueError(
201
- f"halogen must be one of {list(_HALOGEN_ATOMIC_NUM)}, got {halogen!r}"
213
+ f"halogen must be one of {list(_HALOGEN_ATOMIC_NUM)}, got {halogen!r}. "
214
+ f"Use 'H' for hydrocarbon (CH2) backbone series."
202
215
  )
203
216
  if base_repeating is None:
204
217
  base_repeating = ['C']
@@ -646,7 +646,7 @@ def generate_random_mol(n, functional_groups=None, perfluorinated=True, cycle=Fa
646
646
  mol = append_functional_groups(mol, _fg, chain_n=n,
647
647
  **{k: v for k, v in kwargs.items() if k not in _skip_keys})
648
648
  if _max_defs is not None:
649
- from PFASGroups import parse_mol as _parse_mol # lazy import – avoids circular dependency
649
+ from PFASGroups import parse_mol as _parse_mol # lazy import - avoids circular dependency
650
650
  _result = _parse_mol(mol, include_PFAS_definitions=True)
651
651
  _detected = {m['id'] for m in _result.get('matches', []) if m.get('type') == 'PFASdefinition'}
652
652
  if len(_detected) <= _max_defs:
@@ -36,7 +36,7 @@ def get_compiled_HalogenGroups(**kwargs):
36
36
 
37
37
  Examples
38
38
  --------
39
- >>> from PFASgroups import get_compiled_HalogenGroups, HalogenGroup, parse_smiles
39
+ >>> from PFASGroups import get_compiled_HalogenGroups, HalogenGroup, parse_smiles
40
40
  >>> groups = get_compiled_HalogenGroups()
41
41
  >>> groups.append(HalogenGroup(
42
42
  ... id=200, name="Perfluoroalkyl nitrates",
@@ -1,4 +1,4 @@
1
- """HomologueSeries – result container for :func:`~PFASGroups.generate_homologues`.
1
+ """HomologueSeries - result container for :func:`~PFASGroups.generate_homologues`.
2
2
 
3
3
  Backward-compatible with the raw ``{InChIKey: {formula: mol}}`` dict that
4
4
  ``generate_homologues`` previously returned. All existing code that iterates
@@ -6,10 +6,10 @@ or indexes the dict continues to work unchanged.
6
6
 
7
7
  The class adds:
8
8
 
9
- - :meth:`summary` / :meth:`summarise` – coloured text summary
10
- - :meth:`show` / :meth:`plot` – grid image of all homologues
11
- - :meth:`svg` – export to an SVG file
12
- - :meth:`to_sql` – persist to SQLite or PostgreSQL
9
+ - :meth:`summary` / :meth:`summarise` - coloured text summary
10
+ - :meth:`show` / :meth:`plot` - grid image of all homologues
11
+ - :meth:`svg` - export to an SVG file
12
+ - :meth:`to_sql` - persist to SQLite or PostgreSQL
13
13
 
14
14
  Example
15
15
  -------
@@ -46,7 +46,7 @@ _HOMOLOGUE_COLOR = _HOMOLOGUE_COLOUR # US alias
46
46
 
47
47
 
48
48
  # ---------------------------------------------------------------------------
49
- # HomologueEntry – lightweight descriptor for a single homologue
49
+ # HomologueEntry - lightweight descriptor for a single homologue
50
50
  # ---------------------------------------------------------------------------
51
51
 
52
52
  @dataclass
@@ -86,7 +86,7 @@ class HomologueEntry:
86
86
 
87
87
 
88
88
  # ---------------------------------------------------------------------------
89
- # HomologueSeries – dict subclass (backward-compatible) with rich API
89
+ # HomologueSeries - dict subclass (backward-compatible) with rich API
90
90
  # ---------------------------------------------------------------------------
91
91
 
92
92
  class HomologueSeries(dict):
@@ -116,7 +116,7 @@ class HomologueSeries(dict):
116
116
 
117
117
  def __init__(self, *args, **kwargs):
118
118
  super().__init__(*args, **kwargs)
119
- # Metadata – populated by generate_homologues after construction
119
+ # Metadata - populated by generate_homologues after construction
120
120
  self.parent_mol: Optional[Chem.Mol] = None
121
121
  self.parent_smiles: Optional[str] = None
122
122
  self.parent_formula: Optional[str] = None