masster 0.5.8__tar.gz → 0.5.10__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.

Potentially problematic release.


This version of masster might be problematic. Click here for more details.

Files changed (95) hide show
  1. {masster-0.5.8 → masster-0.5.10}/PKG-INFO +3 -1
  2. {masster-0.5.8 → masster-0.5.10}/pyproject.toml +3 -1
  3. {masster-0.5.8 → masster-0.5.10}/src/masster/_version.py +1 -1
  4. {masster-0.5.8 → masster-0.5.10}/src/masster/logger.py +58 -43
  5. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/adducts.py +2 -2
  6. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/h5.py +1 -1
  7. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/helpers.py +47 -15
  8. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/plot.py +706 -578
  9. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/processing.py +4 -4
  10. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/sample.py +91 -48
  11. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/save.py +5 -5
  12. {masster-0.5.8 → masster-0.5.10}/src/masster/study/h5.py +32 -14
  13. {masster-0.5.8 → masster-0.5.10}/src/masster/study/helpers.py +27 -8
  14. {masster-0.5.8 → masster-0.5.10}/src/masster/study/id.py +3 -3
  15. {masster-0.5.8 → masster-0.5.10}/src/masster/study/load.py +1 -164
  16. {masster-0.5.8 → masster-0.5.10}/src/masster/study/merge.py +6 -12
  17. {masster-0.5.8 → masster-0.5.10}/src/masster/study/plot.py +105 -35
  18. {masster-0.5.8 → masster-0.5.10}/src/masster/study/processing.py +7 -7
  19. {masster-0.5.8 → masster-0.5.10}/src/masster/study/study5_schema.json +3 -0
  20. {masster-0.5.8 → masster-0.5.10}/uv.lock +79 -16
  21. {masster-0.5.8 → masster-0.5.10}/.github/workflows/publish.yml +0 -0
  22. {masster-0.5.8 → masster-0.5.10}/.github/workflows/security.yml +0 -0
  23. {masster-0.5.8 → masster-0.5.10}/.github/workflows/test.yml +0 -0
  24. {masster-0.5.8 → masster-0.5.10}/.gitignore +0 -0
  25. {masster-0.5.8 → masster-0.5.10}/.pre-commit-config.yaml +0 -0
  26. {masster-0.5.8 → masster-0.5.10}/LICENSE +0 -0
  27. {masster-0.5.8 → masster-0.5.10}/Makefile +0 -0
  28. {masster-0.5.8 → masster-0.5.10}/README.md +0 -0
  29. {masster-0.5.8 → masster-0.5.10}/TESTING.md +0 -0
  30. {masster-0.5.8 → masster-0.5.10}/demo/example_batch_process.py +0 -0
  31. {masster-0.5.8 → masster-0.5.10}/demo/example_sample_process.py +0 -0
  32. {masster-0.5.8 → masster-0.5.10}/src/masster/__init__.py +0 -0
  33. {masster-0.5.8 → masster-0.5.10}/src/masster/chromatogram.py +0 -0
  34. {masster-0.5.8 → masster-0.5.10}/src/masster/data/dda/20250530_VH_IQX_KW_RP_HSST3_100mm_12min_pos_v4_DDA_OT_C-MiLUT_QC_dil2_01_20250602151849.sample5 +0 -0
  35. {masster-0.5.8 → masster-0.5.10}/src/masster/data/dda/20250530_VH_IQX_KW_RP_HSST3_100mm_12min_pos_v4_DDA_OT_C-MiLUT_QC_dil3_01_20250602150634.sample5 +0 -0
  36. {masster-0.5.8 → masster-0.5.10}/src/masster/data/dda/20250530_VH_IQX_KW_RP_HSST3_100mm_12min_pos_v4_MS1_C-MiLUT_C008_v6_r38_01.sample5 +0 -0
  37. {masster-0.5.8 → masster-0.5.10}/src/masster/data/dda/20250530_VH_IQX_KW_RP_HSST3_100mm_12min_pos_v4_MS1_C-MiLUT_C008_v7_r37_01.sample5 +0 -0
  38. {masster-0.5.8 → masster-0.5.10}/src/masster/data/dda/20250530_VH_IQX_KW_RP_HSST3_100mm_12min_pos_v4_MS1_C-MiLUT_C017_v5_r99_01.sample5 +0 -0
  39. {masster-0.5.8 → masster-0.5.10}/src/masster/data/libs/aa.csv +0 -0
  40. {masster-0.5.8 → masster-0.5.10}/src/masster/data/libs/ccm.csv +0 -0
  41. {masster-0.5.8 → masster-0.5.10}/src/masster/data/libs/hilic.csv +0 -0
  42. {masster-0.5.8 → masster-0.5.10}/src/masster/data/libs/urine.csv +0 -0
  43. {masster-0.5.8 → masster-0.5.10}/src/masster/data/wiff/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.timeseries.data +0 -0
  44. {masster-0.5.8 → masster-0.5.10}/src/masster/data/wiff/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.wiff +0 -0
  45. {masster-0.5.8 → masster-0.5.10}/src/masster/data/wiff/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.wiff.scan +0 -0
  46. {masster-0.5.8 → masster-0.5.10}/src/masster/data/wiff/2025_01_14_VW_7600_LpMx_DBS_CID_2min_TOP15_030msecMS1_005msecReac_CE35_DBS-ON_3.wiff2 +0 -0
  47. {masster-0.5.8 → masster-0.5.10}/src/masster/lib/__init__.py +0 -0
  48. {masster-0.5.8 → masster-0.5.10}/src/masster/lib/lib.py +0 -0
  49. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/__init__.py +0 -0
  50. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/defaults/__init__.py +0 -0
  51. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/defaults/find_adducts_def.py +0 -0
  52. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/defaults/find_features_def.py +0 -0
  53. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/defaults/find_ms2_def.py +0 -0
  54. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/defaults/get_spectrum_def.py +0 -0
  55. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/defaults/sample_def.py +0 -0
  56. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/lib.py +0 -0
  57. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/load.py +0 -0
  58. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/parameters.py +0 -0
  59. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/quant.py +0 -0
  60. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/sample5_schema.json +0 -0
  61. {masster-0.5.8 → masster-0.5.10}/src/masster/sample/sciex.py +0 -0
  62. {masster-0.5.8 → masster-0.5.10}/src/masster/spectrum.py +0 -0
  63. {masster-0.5.8 → masster-0.5.10}/src/masster/study/__init__.py +0 -0
  64. {masster-0.5.8 → masster-0.5.10}/src/masster/study/analysis.py +0 -0
  65. {masster-0.5.8 → masster-0.5.10}/src/masster/study/defaults/__init__.py +0 -0
  66. {masster-0.5.8 → masster-0.5.10}/src/masster/study/defaults/align_def.py +0 -0
  67. {masster-0.5.8 → masster-0.5.10}/src/masster/study/defaults/export_def.py +0 -0
  68. {masster-0.5.8 → masster-0.5.10}/src/masster/study/defaults/fill_def.py +0 -0
  69. {masster-0.5.8 → masster-0.5.10}/src/masster/study/defaults/find_consensus_def.py +0 -0
  70. {masster-0.5.8 → masster-0.5.10}/src/masster/study/defaults/find_ms2_def.py +0 -0
  71. {masster-0.5.8 → masster-0.5.10}/src/masster/study/defaults/identify_def.py +0 -0
  72. {masster-0.5.8 → masster-0.5.10}/src/masster/study/defaults/integrate_chrom_def.py +0 -0
  73. {masster-0.5.8 → masster-0.5.10}/src/masster/study/defaults/integrate_def.py +0 -0
  74. {masster-0.5.8 → masster-0.5.10}/src/masster/study/defaults/merge_def.py +0 -0
  75. {masster-0.5.8 → masster-0.5.10}/src/masster/study/defaults/study_def.py +0 -0
  76. {masster-0.5.8 → masster-0.5.10}/src/masster/study/export.py +0 -0
  77. {masster-0.5.8 → masster-0.5.10}/src/masster/study/parameters.py +0 -0
  78. {masster-0.5.8 → masster-0.5.10}/src/masster/study/save.py +0 -0
  79. {masster-0.5.8 → masster-0.5.10}/src/masster/study/study.py +0 -0
  80. {masster-0.5.8 → masster-0.5.10}/src/masster/wizard/README.md +0 -0
  81. {masster-0.5.8 → masster-0.5.10}/src/masster/wizard/__init__.py +0 -0
  82. {masster-0.5.8 → masster-0.5.10}/src/masster/wizard/example.py +0 -0
  83. {masster-0.5.8 → masster-0.5.10}/src/masster/wizard/wizard.py +0 -0
  84. {masster-0.5.8 → masster-0.5.10}/tests/conftest.py +0 -0
  85. {masster-0.5.8 → masster-0.5.10}/tests/test_chromatogram.py +0 -0
  86. {masster-0.5.8 → masster-0.5.10}/tests/test_defaults.py +0 -0
  87. {masster-0.5.8 → masster-0.5.10}/tests/test_imports.py +0 -0
  88. {masster-0.5.8 → masster-0.5.10}/tests/test_integration.py +0 -0
  89. {masster-0.5.8 → masster-0.5.10}/tests/test_logger.py +0 -0
  90. {masster-0.5.8 → masster-0.5.10}/tests/test_parameters.py +0 -0
  91. {masster-0.5.8 → masster-0.5.10}/tests/test_sample.py +0 -0
  92. {masster-0.5.8 → masster-0.5.10}/tests/test_spectrum.py +0 -0
  93. {masster-0.5.8 → masster-0.5.10}/tests/test_study.py +0 -0
  94. {masster-0.5.8 → masster-0.5.10}/tests/test_version.py +0 -0
  95. {masster-0.5.8 → masster-0.5.10}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: masster
3
- Version: 0.5.8
3
+ Version: 0.5.10
4
4
  Summary: Mass spectrometry data analysis package
5
5
  Project-URL: homepage, https://github.com/zamboni-lab/masster
6
6
  Project-URL: repository, https://github.com/zamboni-lab/masster
@@ -685,6 +685,7 @@ Requires-Dist: altair>=5.5.0
685
685
  Requires-Dist: bokeh>=3.7.3
686
686
  Requires-Dist: cmap>=0.6.2
687
687
  Requires-Dist: datashader>=0.18.1
688
+ Requires-Dist: get-gecko-driver>=1.4
688
689
  Requires-Dist: h5py>=3.14.0
689
690
  Requires-Dist: hdbscan>=0.8.40
690
691
  Requires-Dist: holoviews>=1.21.0
@@ -703,6 +704,7 @@ Requires-Dist: scikit-learn>=1.7.1
703
704
  Requires-Dist: scipy>=1.12.0
704
705
  Requires-Dist: tqdm>=4.65.0
705
706
  Requires-Dist: umap-learn>=0.5.9.post2
707
+ Requires-Dist: webdriver-manager>=4.0.2
706
708
  Provides-Extra: dev
707
709
  Requires-Dist: bandit>=1.7.0; extra == 'dev'
708
710
  Requires-Dist: black>=23.0.0; extra == 'dev'
@@ -1,7 +1,7 @@
1
1
 
2
2
  [project]
3
3
  name = "masster"
4
- version = "0.5.8"
4
+ version = "0.5.10"
5
5
  description = "Mass spectrometry data analysis package"
6
6
  authors = [
7
7
  { name = "Zamboni Lab" }
@@ -49,6 +49,8 @@ dependencies = [
49
49
  "scikit-learn>=1.7.1",
50
50
  "umap-learn>=0.5.9.post2",
51
51
  "hdbscan>=0.8.40",
52
+ "get-gecko-driver>=1.4",
53
+ "webdriver-manager>=4.0.2",
52
54
  ]
53
55
 
54
56
  [project.optional-dependencies]
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
 
4
- __version__ = "0.5.8"
4
+ __version__ = "0.5.10"
5
5
 
6
6
 
7
7
  def get_version():
@@ -3,15 +3,15 @@
3
3
  Simple logger system for masster Study and Sample instances.
4
4
  Uses basic Python logging timestamp = dt.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
5
5
 
6
- # Loguru-style colors for different log levels
6
+ # Universal colors compatible with both dark and light themes
7
7
  level_colors = {
8
- 'TRACE': '\x1b[34m', # blue
9
- 'DEBUG': '\x1b[36m', # cyan
10
- 'INFO': '\x1b[37m', # white
11
- 'SUCCESS': '\x1b[32m', # green
12
- 'WARNING': '\x1b[33m', # yellow
13
- 'ERROR': '\x1b[31m', # red
14
- 'CRITICAL': '\x1b[35m', # magenta
8
+ 'TRACE': '\x1b[94m', # bright blue (readable on both dark/light)
9
+ 'DEBUG': '\x1b[96m', # bright cyan (readable on both dark/light)
10
+ 'INFO': '\x1b[90m', # bright black/gray (readable on both dark/light)
11
+ 'SUCCESS': '\x1b[92m', # bright green (readable on both dark/light)
12
+ 'WARNING': '\x1b[93m', # bright yellow (readable on both dark/light)
13
+ 'ERROR': '\x1b[91m', # bright red (readable on both dark/light)
14
+ 'CRITICAL': '\x1b[95m', # bright magenta (readable on both dark/light)
15
15
  }
16
16
 
17
17
  level_str = record.levelname.ljust(8)complex loguru filtering.
@@ -102,19 +102,19 @@ class MassterLogger:
102
102
  dt = datetime.datetime.fromtimestamp(record.created)
103
103
  timestamp = dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] # Remove last 3 digits for milliseconds
104
104
 
105
- # Loguru-style colors for different log levels
105
+ # Universal colors compatible with both dark and light themes
106
106
  level_colors = {
107
- "TRACE": "\x1b[34m", # blue
108
- "DEBUG": "\x1b[36m", # cyan
109
- "INFO": "\x1b[37m", # white
110
- "SUCCESS": "\x1b[32m", # green
111
- "WARNING": "\x1b[33m", # yellow
112
- "ERROR": "\x1b[31m", # red
113
- "CRITICAL": "\x1b[35m", # magenta
107
+ "TRACE": "\x1b[94m", # bright blue (readable on both dark/light)
108
+ "DEBUG": "\x1b[96m", # bright cyan (readable on both dark/light)
109
+ "INFO": "\x1b[90m", # bright black/gray (readable on both dark/light)
110
+ "SUCCESS": "\x1b[92m", # bright green (readable on both dark/light)
111
+ "WARNING": "\x1b[93m", # bright yellow (readable on both dark/light)
112
+ "ERROR": "\x1b[91m", # bright red (readable on both dark/light)
113
+ "CRITICAL": "\x1b[95m", # bright magenta (readable on both dark/light)
114
114
  }
115
115
 
116
116
  level_str = record.levelname.ljust(8)
117
- level_color = level_colors.get(record.levelname, "\x1b[37m") # default white
117
+ level_color = level_colors.get(record.levelname, "\x1b[90m") # default to gray instead of white
118
118
  label_part = self.label + " | " if self.label else ""
119
119
 
120
120
  # For DEBUG and TRACE levels, add module/location information
@@ -133,9 +133,9 @@ class MassterLogger:
133
133
  f"\x1b[90m{module_name}:{func_name}:{line_no}\x1b[0m | " # dim gray for location info
134
134
  )
135
135
 
136
- # Loguru-style format: <white>timestamp</white> | <level>LEVEL</level> | <location> | <cyan>label</cyan> - <level>message</level>
136
+ # Universal format: timestamp | level | location | label - message
137
137
  return (
138
- f"\x1b[37m{timestamp}\x1b[0m | " # white timestamp
138
+ f"\x1b[90m{timestamp}\x1b[0m | " # gray timestamp (universal for both themes)
139
139
  f"{level_color}{level_str}\x1b[0m | " # colored level
140
140
  f"{location_info}" # location info for DEBUG/TRACE
141
141
  f"{level_color}{label_part}\x1b[0m" # colored label
@@ -181,19 +181,19 @@ class MassterLogger:
181
181
  dt = datetime.datetime.fromtimestamp(record.created)
182
182
  timestamp = dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
183
183
 
184
- # Loguru-style colors for different log levels
184
+ # Universal colors compatible with both dark and light themes
185
185
  level_colors = {
186
- "TRACE": "\x1b[34m", # blue
187
- "DEBUG": "\x1b[36m", # cyan
188
- "INFO": "\x1b[37m", # white
189
- "SUCCESS": "\x1b[32m", # green
190
- "WARNING": "\x1b[33m", # yellow
191
- "ERROR": "\x1b[31m", # red
192
- "CRITICAL": "\x1b[35m", # magenta
186
+ "TRACE": "\x1b[94m", # bright blue (readable on both dark/light)
187
+ "DEBUG": "\x1b[96m", # bright cyan (readable on both dark/light)
188
+ "INFO": "\x1b[90m", # bright black/gray (readable on both dark/light)
189
+ "SUCCESS": "\x1b[92m", # bright green (readable on both dark/light)
190
+ "WARNING": "\x1b[93m", # bright yellow (readable on both dark/light)
191
+ "ERROR": "\x1b[91m", # bright red (readable on both dark/light)
192
+ "CRITICAL": "\x1b[95m", # bright magenta (readable on both dark/light)
193
193
  }
194
194
 
195
195
  level_str = record.levelname.ljust(8)
196
- level_color = level_colors.get(record.levelname, "\x1b[37m") # default white
196
+ level_color = level_colors.get(record.levelname, "\x1b[90m") # default to gray instead of white
197
197
  label_part = self.label + " | " if self.label else ""
198
198
 
199
199
  # For DEBUG and TRACE levels, add module/location information
@@ -212,9 +212,9 @@ class MassterLogger:
212
212
  f"\x1b[90m{module_name}:{func_name}:{line_no}\x1b[0m | " # dim gray for location info
213
213
  )
214
214
 
215
- # Loguru-style format: <white>timestamp</white> | <level>LEVEL</level> | <location> | <cyan>label</cyan> - <level>message</level>
215
+ # Universal format: timestamp | level | location | label - message
216
216
  return (
217
- f"\x1b[37m{timestamp}\x1b[0m | " # white timestamp
217
+ f"\x1b[90m{timestamp}\x1b[0m | " # gray timestamp (universal for both themes)
218
218
  f"{level_color}{level_str}\x1b[0m | " # colored level
219
219
  f"{location_info}" # location info for DEBUG/TRACE
220
220
  f"{level_color}{label_part}\x1b[0m" # colored label
@@ -245,19 +245,19 @@ class MassterLogger:
245
245
  dt = datetime.datetime.fromtimestamp(record.created)
246
246
  timestamp = dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
247
247
 
248
- # Loguru-style colors for different log levels
248
+ # Universal colors compatible with both dark and light themes
249
249
  level_colors = {
250
- "TRACE": "\x1b[34m", # blue
251
- "DEBUG": "\x1b[36m", # cyan
252
- "INFO": "\x1b[37m", # white
253
- "SUCCESS": "\x1b[32m", # green
254
- "WARNING": "\x1b[33m", # yellow
255
- "ERROR": "\x1b[31m", # red
256
- "CRITICAL": "\x1b[35m", # magenta
250
+ "TRACE": "\x1b[94m", # bright blue (readable on both dark/light)
251
+ "DEBUG": "\x1b[96m", # bright cyan (readable on both dark/light)
252
+ "INFO": "\x1b[90m", # bright black/gray (readable on both dark/light)
253
+ "SUCCESS": "\x1b[92m", # bright green (readable on both dark/light)
254
+ "WARNING": "\x1b[93m", # bright yellow (readable on both dark/light)
255
+ "ERROR": "\x1b[91m", # bright red (readable on both dark/light)
256
+ "CRITICAL": "\x1b[95m", # bright magenta (readable on both dark/light)
257
257
  }
258
258
 
259
259
  level_str = record.levelname.ljust(8)
260
- level_color = level_colors.get(record.levelname, "\x1b[37m") # default white
260
+ level_color = level_colors.get(record.levelname, "\x1b[90m") # default to gray instead of white
261
261
  label_part = self.label + " | " if self.label else ""
262
262
 
263
263
  # For DEBUG and TRACE levels, add module/location information
@@ -276,9 +276,9 @@ class MassterLogger:
276
276
  f"\x1b[90m{module_name}:{func_name}:{line_no}\x1b[0m | " # dim gray for location info
277
277
  )
278
278
 
279
- # Loguru-style format: <white>timestamp</white> | <level>LEVEL</level> | <location> | <cyan>label</cyan> - <level>message</level>
279
+ # Universal format: timestamp | level | location | label - message
280
280
  return (
281
- f"\x1b[37m{timestamp}\x1b[0m | " # white timestamp
281
+ f"\x1b[90m{timestamp}\x1b[0m | " # gray timestamp (universal for both themes)
282
282
  f"{level_color}{level_str}\x1b[0m | " # colored level
283
283
  f"{location_info}" # location info for DEBUG/TRACE
284
284
  f"{level_color}{label_part}\x1b[0m" # colored label
@@ -332,8 +332,22 @@ class MassterLogger:
332
332
  self.logger_instance.info(message, *args, **kwargs)
333
333
 
334
334
  def success(self, message: str, *args, **kwargs):
335
- """Log a SUCCESS level message (mapped to INFO)."""
336
- self.info(message, *args, **kwargs)
335
+ """Log a SUCCESS level message (custom level)."""
336
+ # Create a custom log record with SUCCESS level
337
+ import logging
338
+
339
+ # Create a LogRecord manually with SUCCESS level
340
+ record = self.logger_instance.makeRecord(
341
+ self.logger_instance.name,
342
+ logging.INFO, # Use INFO level for Python's filtering
343
+ "", 0, message, args, None, func="success"
344
+ )
345
+ # Override the levelname for display
346
+ record.levelname = "SUCCESS"
347
+
348
+ # Handle the record directly through our handler
349
+ if self.handler:
350
+ self.handler.handle(record)
337
351
 
338
352
  def warning(self, message: str, *args, **kwargs):
339
353
  """Log a WARNING level message."""
@@ -372,3 +386,4 @@ class MassterLogger:
372
386
 
373
387
  def __repr__(self):
374
388
  return f"MassterLogger(type={self.instance_type}, id={self.instance_id}, level={self.level})"
389
+
@@ -798,8 +798,8 @@ def find_adducts(self, **kwargs):
798
798
  total_with_adducts = sum(1 for x in final_adducts if x is not None)
799
799
  total_groups = max(final_groups) if final_groups else 0
800
800
 
801
- self.logger.info(
802
- f"Adduct detection completed: {total_with_adducts} features with adducts in {total_groups} groups",
801
+ self.logger.success(
802
+ f"Adduct detection completed. Features with adducts: {total_with_adducts}. Adduct groups: {total_groups}.",
803
803
  )
804
804
 
805
805
  # Store parameters including the actual processed adducts list
@@ -295,7 +295,7 @@ def _save_sample5(
295
295
 
296
296
  # Store lib and lib_match - removed (no longer saving lib data)
297
297
 
298
- self.logger.info(f"Sample saved to {filename}")
298
+ self.logger.success(f"Sample saved to {filename}")
299
299
  if save_featurexml:
300
300
  # Get or recreate the feature map if needed
301
301
  feature_map = self._get_feature_map()
@@ -360,6 +360,7 @@ def get_eic(self, mz, mz_tol=None):
360
360
 
361
361
  def features_select(
362
362
  self,
363
+ uid=None,
363
364
  mz=None,
364
365
  rt=None,
365
366
  coherence=None,
@@ -369,15 +370,16 @@ def features_select(
369
370
  iso_of=None,
370
371
  has_MS2=None,
371
372
  prominence_scaled=None,
372
- height_scaled=None,
373
373
  prominence=None,
374
+ height_scaled=None,
374
375
  height=None,
375
- uids=None,
376
+ adduct_group=None,
376
377
  ):
377
378
  """
378
379
  Select features based on specified criteria and return the filtered DataFrame.
379
380
 
380
381
  Parameters:
382
+ uid: feature UID filter (list of feature UIDs, tuple for range of feature UIDs, polars/pandas DataFrame with feature_uid/feature_id column, or None for all features)
381
383
  mz: m/z range filter (tuple for range, single value for minimum)
382
384
  rt: retention time range filter (tuple for range, single value for minimum)
383
385
  coherence: chromatogram coherence filter (tuple for range, single value for minimum)
@@ -390,8 +392,7 @@ def features_select(
390
392
  height_scaled: scaled height filter (tuple for range, single value for minimum)
391
393
  prominence: prominence filter (tuple for range, single value for minimum)
392
394
  height: height filter (tuple for range, single value for minimum)
393
- uids: feature UID filter (list of feature UIDs, polars/pandas DataFrame with feature_uid/feature_id column, or None for all features)
394
-
395
+ adduct_group: adduct group filter (single value for exact match, list of values for multiple groups, tuple for range, or None for all)
395
396
  Returns:
396
397
  polars.DataFrame: Filtered features DataFrame
397
398
  """
@@ -402,17 +403,29 @@ def features_select(
402
403
  feats = self.features_df.clone()
403
404
 
404
405
  # Filter by feature UIDs if provided
405
- if uids is not None:
406
- feature_uids_to_keep = self._get_feature_uids(features=uids, verbose=True)
407
- if not feature_uids_to_keep:
408
- self.logger.warning("No valid feature UIDs provided.")
409
- return feats.limit(0) # Return empty DataFrame with same structure
410
-
411
- feats_len_before_filter = len(feats)
412
- feats = feats.filter(pl.col("feature_uid").is_in(feature_uids_to_keep))
413
- self.logger.debug(
414
- f"Selected features by UIDs. Features removed: {feats_len_before_filter - len(feats)}",
415
- )
406
+ if uid is not None:
407
+ if isinstance(uid, tuple) and len(uid) == 2:
408
+ # Handle tuple as range of feature UIDs
409
+ min_uid, max_uid = uid
410
+ feats_len_before_filter = len(feats)
411
+ feats = feats.filter(
412
+ (pl.col("feature_uid") >= min_uid) & (pl.col("feature_uid") <= max_uid)
413
+ )
414
+ self.logger.debug(
415
+ f"Selected features by UID range ({min_uid}-{max_uid}). Features removed: {feats_len_before_filter - len(feats)}",
416
+ )
417
+ else:
418
+ # Handle list or DataFrame input
419
+ feature_uids_to_keep = self._get_feature_uids(features=uid, verbose=True)
420
+ if not feature_uids_to_keep:
421
+ self.logger.warning("No valid feature UIDs provided.")
422
+ return feats.limit(0) # Return empty DataFrame with same structure
423
+
424
+ feats_len_before_filter = len(feats)
425
+ feats = feats.filter(pl.col("feature_uid").is_in(feature_uids_to_keep))
426
+ self.logger.debug(
427
+ f"Selected features by UIDs. Features removed: {feats_len_before_filter - len(feats)}",
428
+ )
416
429
 
417
430
  if coherence is not None:
418
431
  has_coherence = "chrom_coherence" in self.features_df.columns
@@ -579,6 +592,25 @@ def features_select(
579
592
  self.logger.debug(
580
593
  f"Selected features by {height_col}. Features removed: {feats_len_before_filter - len(feats)}",
581
594
  )
595
+
596
+ if adduct_group is not None:
597
+ feats_len_before_filter = len(feats)
598
+ if "adduct_group" not in feats.columns:
599
+ self.logger.warning("No adduct_group data found in features.")
600
+ else:
601
+ if isinstance(adduct_group, tuple) and len(adduct_group) == 2:
602
+ min_adduct_group, max_adduct_group = adduct_group
603
+ feats = feats.filter(
604
+ (pl.col("adduct_group") >= min_adduct_group) & (pl.col("adduct_group") <= max_adduct_group)
605
+ )
606
+ elif isinstance(adduct_group, list):
607
+ feats = feats.filter(pl.col("adduct_group").is_in(adduct_group))
608
+ else:
609
+ feats = feats.filter(pl.col("adduct_group") == adduct_group)
610
+ self.logger.debug(
611
+ f"Selected features by adduct_group. Features removed: {feats_len_before_filter - len(feats)}",
612
+ )
613
+
582
614
  if len(feats) == 0:
583
615
  self.logger.warning("No features remaining after applying selection criteria.")
584
616
  else: