Skip to content

Commit b2e2b6c

Browse files
committed
Merge branch '242_low_spectrum_edge_warning' into 'dev'
Fix resolving power at spectrum edges without warning spam See merge request mass-spectrometry/corems!258
2 parents 9f988d8 + 352848c commit b2e2b6c

3 files changed

Lines changed: 116 additions & 48 deletions

File tree

‎corems/encapsulation/factory/processingSetting.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ class MassSpectrumSetting:
585585
Maximum m/z to use for peak picking. Default is 1200.0.
586586
picking_point_extrapolate : int, optional
587587
How many data points (in each direction) to extrapolate the mz axis and 0 pad the abundance axis. Default is 3.
588-
Recommend 3 for reduced profile data or if peak picking faults
588+
Recommend not changing this value unless necessary.
589589
calib_minimize_method : str, optional
590590
Minimization method to use for calibration. Default is 'Powell'.
591591
calib_pol_order : int, optional

‎corems/mass_spectrum/calc/PeakPicking.py‎

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,9 @@ def calculate_resolving_power(self, intes, massa, current_index):
396396
Returns
397397
-------
398398
float
399-
The resolving power of the peak.
399+
The resolving power of the peak, or ``nan`` if a half-maximum
400+
crossing does not exist on both sides (spectrum edge truncates
401+
the peak).
400402
401403
Notes
402404
--------
@@ -408,37 +410,19 @@ def calculate_resolving_power(self, intes, massa, current_index):
408410

409411
peak_height = intes[current_index]
410412
target_peak_height = peak_height / 2
413+
n_points = len(intes)
411414

412-
peak_height_minus = peak_height
413-
peak_height_plus = peak_height
414-
415-
# There are issues when a peak is at the high or low limit of a spectrum in finding its local minima and maxima
416-
# This solution will return nan for resolving power when a peak is possibly too close to an edge to avoid the issue
417-
418-
if current_index < 5:
419-
warnings.warn("peak at low spectrum edge, returning no resolving power")
420-
return nan
421-
elif abs(current_index - len(intes)) < 5:
422-
warnings.warn("peak at high spectrum edge, returning no resolving power")
415+
index_minus = current_index
416+
while index_minus > 0 and intes[index_minus] >= target_peak_height:
417+
index_minus -= 1
418+
if intes[index_minus] >= target_peak_height:
423419
return nan
424-
else:
425-
pass
426420

427-
index_minus = current_index
428-
while peak_height_minus >= target_peak_height:
429-
index_minus = index_minus - 1
430-
if index_minus < 0:
431-
warnings.warn(
432-
"Res. calc. warning - peak index minus adjacent to spectrum edge \n \
433-
Zeroing the first 5 data points of abundance. Peaks at spectrum edge may be incorrectly reported \n \
434-
Perhaps try to increase picking_point_extrapolate (e.g. to 3)"
435-
)
436-
# Pad the first 5 data points with zeros and restart the loop
437-
intes[:5] = 0
438-
peak_height_minus = target_peak_height
439-
index_minus = current_index
440-
else:
441-
peak_height_minus = intes[index_minus]
421+
index_plus = current_index
422+
while index_plus < n_points - 1 and intes[index_plus] >= target_peak_height:
423+
index_plus += 1
424+
if intes[index_plus] >= target_peak_height:
425+
return nan
442426

443427
if self.mspeaks_settings.legacy_centroid_polyfit:
444428
x = [massa[index_minus], massa[index_minus + 1]]
@@ -459,23 +443,6 @@ def calculate_resolving_power(self, intes, massa, current_index):
459443
y_intercept = target_peak_height
460444
massa1 = (y_intercept - b) / a
461445

462-
index_plus = current_index
463-
while peak_height_plus >= target_peak_height:
464-
index_plus = index_plus + 1
465-
466-
try:
467-
peak_height_plus = intes[index_plus]
468-
except IndexError:
469-
warnings.warn(
470-
"Res. calc. warning - peak index plus adjacent to spectrum edge \n \
471-
Zeroing the last 5 data points of abundance. Peaks at spectrum edge may be incorrectly reported\
472-
Perhaps try to increase picking_point_extrapolate (e.g. to 3)"
473-
)
474-
# Pad the first 5 data points with zeros and restart the loop
475-
intes[-5:] = 0
476-
peak_height_plus = target_peak_height
477-
index_plus = current_index
478-
479446
if self.mspeaks_settings.legacy_centroid_polyfit:
480447
x = [massa[index_plus], massa[index_plus - 1]]
481448
y = [intes[index_plus], intes[index_plus - 1]]

‎tests/test_mass_spectrum.py‎

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import warnings
2+
from types import SimpleNamespace
3+
4+
from numpy import allclose, arange, array, copy, isfinite, isnan
5+
6+
from corems.mass_spectrum.calc.PeakPicking import PeakPicking
7+
18
# Tests for adpodization methods
29
def test_hamming(bruker_transient):
310
"""Test the creation of a mass spectrum object with the Hamming apodization method"""
@@ -103,4 +110,98 @@ def test_mass_spectrum_properties(mass_spectrum_ftms):
103110
res = mass_spectrum_ftms.get_mz_and_abundance_peaks_tuples()
104111
assert len(res) == len(mass_spectrum_ftms)
105112
mass_spectrum_ftms.get_masses_count_by_nominal_mass()
106-
mass_spectrum_ftms.resolving_power_calc(12, 1)
113+
mass_spectrum_ftms.resolving_power_calc(12, 1)
114+
115+
116+
def _resolving_power_picker():
117+
picker = PeakPicking()
118+
picker.mspeaks_settings = SimpleNamespace(
119+
legacy_resolving_power=True,
120+
legacy_centroid_polyfit=False,
121+
)
122+
return picker
123+
124+
125+
def _spectrum_edge_warnings(caught):
126+
needles = (
127+
"peak at low spectrum edge",
128+
"peak at high spectrum edge",
129+
"Zeroing the first 5 data points",
130+
"Zeroing the last 5 data points",
131+
"peak index minus adjacent to spectrum edge",
132+
"peak index plus adjacent to spectrum edge",
133+
)
134+
return [
135+
str(w.message)
136+
for w in caught
137+
if any(n in str(w.message) for n in needles)
138+
]
139+
140+
141+
def test_resolving_power_when_fwhm_is_measurable():
142+
"""Interior and padded-edge peaks get finite RP; abundance is not mutated."""
143+
picker = _resolving_power_picker()
144+
cases = (
145+
(
146+
array([100.0, 100.1, 100.2, 100.3, 100.4]),
147+
array([0.0, 50.0, 100.0, 50.0, 0.0]),
148+
2,
149+
334.0,
150+
),
151+
(
152+
100.0 + 0.1 * arange(10),
153+
array([0.0, 0.0, 0.0, 100.0, 50.0, 0.0, 10.0, 10.0, 10.0, 10.0]),
154+
3,
155+
None,
156+
),
157+
(
158+
100.0 + 0.1 * arange(10),
159+
array([10.0, 10.0, 10.0, 10.0, 0.0, 50.0, 100.0, 50.0, 0.0, 0.0]),
160+
6,
161+
None,
162+
),
163+
)
164+
with warnings.catch_warnings(record=True) as caught:
165+
warnings.simplefilter("always")
166+
for massa, intes, apex, expected in cases:
167+
original = copy(intes)
168+
rp = picker.calculate_resolving_power(intes, massa, apex)
169+
assert isfinite(rp)
170+
assert rp > 0
171+
if expected is not None:
172+
assert allclose(rp, expected)
173+
assert allclose(intes, original)
174+
assert _spectrum_edge_warnings(caught) == []
175+
176+
177+
def test_resolving_power_nan_when_fwhm_truncated():
178+
"""Missing half-max on either side yields silent nan and does not zero abundance."""
179+
picker = _resolving_power_picker()
180+
cases = (
181+
(
182+
100.0 + 0.1 * arange(8),
183+
array([100.0, 50.0, 0.0, 10.0, 10.0, 10.0, 10.0, 10.0]),
184+
0,
185+
),
186+
(
187+
100.0 + 0.1 * arange(8),
188+
array([10.0, 10.0, 10.0, 10.0, 10.0, 0.0, 50.0, 100.0]),
189+
7,
190+
),
191+
(
192+
100.0 + 0.1 * arange(12),
193+
array(
194+
[80.0, 80.0, 80.0, 80.0, 80.0, 80.0, 100.0, 50.0, 0.0, 10.0, 10.0, 10.0]
195+
),
196+
6,
197+
),
198+
)
199+
with warnings.catch_warnings(record=True) as caught:
200+
warnings.simplefilter("always")
201+
for massa, intes, apex in cases:
202+
original = copy(intes)
203+
rp = picker.calculate_resolving_power(intes, massa, apex)
204+
assert isnan(rp)
205+
assert allclose(intes, original)
206+
assert _spectrum_edge_warnings(caught) == []
207+

0 commit comments

Comments
 (0)