Skip to content

Commit 08ceab4

Browse files
Document and test what the XQRS backsearch skip relies on
The skip added in #596 is only correct while _update_noise leaves qrs_thr alone and _update_qrs moves last_qrs_peak_num to the current peak. Name both in the comment and add a test that fails if either changes.
1 parent ff4a7e2 commit 08ceab4

2 files changed

Lines changed: 37 additions & 3 deletions

File tree

‎src/wfdb/processing/qrs.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -610,9 +610,10 @@ def _backsearch(self):
610610
611611
"""
612612
if self.last_qrs_peak_num is not None:
613-
# Skip peaks a previous backsearch already rejected. Their result
614-
# can only change after a QRS update, which moves
615-
# last_qrs_peak_num past them.
613+
# Skip peaks a previous backsearch already rejected. A backsearch result
614+
# depends only on qrs_thr and last_qrs_ind, which change only in
615+
# _update_qrs, and _update_qrs moves last_qrs_peak_num to the current
616+
# peak. If either of these stops being true, remove this skip.
616617
start = max(self.last_qrs_peak_num, self._backsearch_peak_num) + 1
617618
for peak_num in range(start, self.peak_num + 1):
618619
if self._is_qrs(peak_num=peak_num, backsearch=True):

‎tests/test_processing.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,39 @@ def spy(peak_num, backsearch=False):
221221
n_after = np.sum(xqrs.qrs_inds > 150 * fs)
222222
assert n_before > 30 and n_after > 30
223223

224+
def test_xqrs_backsearch_skip_assumptions(self):
225+
"""The backsearch skip relies on how XQRS updates its state (#597)."""
226+
sig, fields = wfdb.rdsamp("sample-data/100", channels=[0])
227+
fs = fields["fs"]
228+
x = sig[:, 0]
229+
rng = np.random.default_rng(0)
230+
gap = 0.01 * rng.standard_normal(120 * fs)
231+
# Scaled-down beats after the gap are only found by backsearch
232+
y = np.concatenate([x[: 30 * fs], gap, 0.4 * x[30 * fs : 60 * fs]])
233+
234+
xqrs = processing.XQRS(sig=y, fs=fs)
235+
update_noise = xqrs._update_noise
236+
update_qrs = xqrs._update_qrs
237+
238+
def noise_spy(peak_num):
239+
qrs_thr = xqrs.qrs_thr
240+
update_noise(peak_num)
241+
# A noise update must not change the result of a backsearch
242+
assert xqrs.qrs_thr == qrs_thr
243+
244+
def qrs_spy(peak_num, backsearch=False):
245+
update_qrs(peak_num, backsearch=backsearch)
246+
# Peaks up to the current one count as already searched
247+
assert xqrs.last_qrs_peak_num == xqrs.peak_num
248+
249+
with (
250+
mock.patch.object(xqrs, "_update_noise", side_effect=noise_spy),
251+
mock.patch.object(xqrs, "_update_qrs", side_effect=qrs_spy),
252+
):
253+
xqrs.detect(verbose=False)
254+
255+
assert len(xqrs.backsearch_qrs_inds) > 0
256+
224257

225258
class TestXQRSDetect:
226259
"""Tests for the xqrs_detect convenience wrapper."""

0 commit comments

Comments
 (0)