Skip to content

Commit c9dfd70

Browse files
committed
Merge branch '286_ph_inten_min_abs' into 'dev'
Add an absolute intensity floor for LC-MS peak picking See merge request mass-spectrometry/corems!272
2 parents eb7363a + 864406f commit c9dfd70

3 files changed

Lines changed: 137 additions & 2 deletions

File tree

‎corems/encapsulation/factory/processingSetting.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@ class LiquidChromatographSetting:
323323
Should be greater to or equal to ph_inten_min_rel.
324324
Called within the PH_Calculations.find_mass_features() method.
325325
Default is 0.001.
326+
ph_inten_min_abs : float, optional
327+
Absolute minimum intensity for points that enter persistent homology.
328+
Applied with ph_inten_min_rel on profile and centroided data.
329+
The relative floor is still a fraction of the unfiltered maximum.
330+
0 disables the absolute floor. Default is 0.0.
326331
mass_feature_cluster_mz_tolerance_rel : float, optional
327332
Relative m/z tolerance to use for clustering mass features.
328333
Used for both "persistent homology" and "centroided_persistent_homology"
@@ -438,6 +443,7 @@ class LiquidChromatographSetting:
438443
ph_smooth_radius_scan = 1
439444
ph_inten_min_rel = 0.001
440445
ph_persis_min_rel = 0.001
446+
ph_inten_min_abs: float = 0.0
441447

442448
# Parameters used to cluster mass features
443449
mass_feature_cluster_mz_tolerance_rel: float = 5e-6

‎corems/mass_spectra/calc/lc_calc.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2141,7 +2141,10 @@ def find_mass_features_ph(self, ms_level=1, grid=True, targeted_search=False, ta
21412141
self.mass_features = {}
21422142
return
21432143
else:
2144-
threshold = self.parameters.lc_ms.ph_inten_min_rel * data.intensity.max()
2144+
threshold = max(
2145+
self.parameters.lc_ms.ph_inten_min_rel * data.intensity.max(),
2146+
self.parameters.lc_ms.ph_inten_min_abs,
2147+
)
21452148
persistence_threshold = (
21462149
self.parameters.lc_ms.ph_persis_min_rel * data.intensity.max()
21472150
)
@@ -2469,7 +2472,10 @@ def find_mass_features_ph_centroid(self, ms_level=1, targeted_search=False, targ
24692472
else:
24702473
# Normal mode with threshold
24712474
max_intensity = data_with_time["intensity"].max()
2472-
threshold = self.parameters.lc_ms.ph_inten_min_rel * max_intensity
2475+
threshold = max(
2476+
self.parameters.lc_ms.ph_inten_min_rel * max_intensity,
2477+
self.parameters.lc_ms.ph_inten_min_abs,
2478+
)
24732479
valid_mask = data_with_time["intensity"].notna() & (data_with_time["intensity"] > threshold)
24742480
required_cols = ["mz", "intensity", "scan", "scan_time"]
24752481
data_thres = data_with_time.loc[valid_mask, required_cols].copy()

‎tests/test_ph_inten_min_abs.py‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""Absolute intensity floor before LC-MS persistent-homology peak picking."""
2+
3+
import pandas as pd
4+
5+
from corems.encapsulation.factory.parameters import LCMSParameters
6+
from corems.encapsulation.factory.processingSetting import (
7+
LiquidChromatographSetting,
8+
settings_to_export_dict,
9+
)
10+
from corems.mass_spectra.calc.lc_calc import PHCalculations
11+
12+
# Max intensity is 1000. A relative floor of 0.1 keeps intensity > 100.
13+
_INTENSITIES = (50.0, 200.0, 1000.0)
14+
15+
16+
def _frame():
17+
data = pd.DataFrame(
18+
{
19+
"mz": [200.0, 201.0, 202.0],
20+
"scan": [1, 1, 1],
21+
"intensity": list(_INTENSITIES),
22+
}
23+
)
24+
scan_df = pd.DataFrame({"scan": [1], "scan_time": [1.0]})
25+
return data, scan_df
26+
27+
28+
def _target_window():
29+
return {
30+
"target_mz_list": [201.0],
31+
"target_rt_list": [1.0],
32+
"mz_tolerance_ppm": 20_000,
33+
"rt_tolerance": 0.5,
34+
}
35+
36+
37+
class _ProfileHost(PHCalculations):
38+
def __init__(self, abs_floor):
39+
data, scan_df = _frame()
40+
self.parameters = LCMSParameters()
41+
self.parameters.lc_ms.ph_inten_min_rel = 0.1
42+
self.parameters.lc_ms.ph_inten_min_abs = abs_floor
43+
self.parameters.lc_ms.verbose_processing = False
44+
self._ms_unprocessed = {1: data}
45+
self.scan_df = scan_df
46+
self.mass_features = {}
47+
self.kept = None
48+
49+
def check_if_grid(self, data):
50+
return True
51+
52+
def _record(self, data_thres):
53+
self.kept = sorted(float(value) for value in data_thres["intensity"])
54+
55+
def _find_mass_features_ph_single(self, data_thres, *args, **kwargs):
56+
self._record(data_thres)
57+
58+
def _find_mass_features_ph_partition(self, data_thres, *args, **kwargs):
59+
self._record(data_thres)
60+
61+
62+
class _CentroidHost(PHCalculations):
63+
def __init__(self, abs_floor):
64+
data, scan_df = _frame()
65+
self.parameters = LCMSParameters()
66+
self.parameters.lc_ms.ph_inten_min_rel = 0.1
67+
self.parameters.lc_ms.ph_inten_min_abs = abs_floor
68+
self.parameters.lc_ms.verbose_processing = False
69+
self._ms_unprocessed = {1: data}
70+
self.scan_df = scan_df
71+
self.mass_features = {}
72+
self.kept = None
73+
74+
def roll_up_dataframe(self, df, **kwargs):
75+
if len(df):
76+
self.kept = sorted(float(value) for value in df["intensity"])
77+
return df.iloc[0:0].copy()
78+
79+
80+
def test_ph_inten_min_abs_defaults_to_zero_and_is_exported():
81+
settings = LiquidChromatographSetting()
82+
assert settings.ph_inten_min_abs == 0.0
83+
assert settings_to_export_dict(settings)["ph_inten_min_abs"] == 0.0
84+
85+
86+
def test_zero_absolute_floor_keeps_the_relative_mask():
87+
profile = _ProfileHost(0.0)
88+
profile.find_mass_features_ph()
89+
centroid = _CentroidHost(0.0)
90+
centroid.find_mass_features_ph_centroid()
91+
assert profile.kept == [200.0, 1000.0]
92+
assert centroid.kept == [200.0, 1000.0]
93+
94+
95+
def test_absolute_floor_drops_points_the_relative_floor_would_keep():
96+
profile = _ProfileHost(500.0)
97+
profile.find_mass_features_ph()
98+
centroid = _CentroidHost(500.0)
99+
centroid.find_mass_features_ph_centroid()
100+
assert profile.kept == [1000.0]
101+
assert centroid.kept == [1000.0]
102+
103+
104+
def test_relative_floor_still_drops_points_above_the_absolute_floor():
105+
profile = _ProfileHost(10.0)
106+
profile.find_mass_features_ph()
107+
centroid = _CentroidHost(10.0)
108+
centroid.find_mass_features_ph_centroid()
109+
assert profile.kept == [200.0, 1000.0]
110+
assert centroid.kept == [200.0, 1000.0]
111+
112+
113+
def test_targeted_search_ignores_the_absolute_floor():
114+
profile = _ProfileHost(500.0)
115+
profile.find_mass_features_ph(
116+
targeted_search=True, target_search_dict=_target_window()
117+
)
118+
centroid = _CentroidHost(500.0)
119+
centroid.find_mass_features_ph_centroid(
120+
targeted_search=True, target_search_dict=_target_window()
121+
)
122+
assert profile.kept == [50.0, 200.0, 1000.0]
123+
assert centroid.kept == [50.0, 200.0, 1000.0]

0 commit comments

Comments
 (0)