Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/wfdb/processing/qrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,10 @@ def _backsearch(self):

"""
if self.last_qrs_peak_num is not None:
# Skip peaks a previous backsearch already rejected. Their result
# can only change after a QRS update, which moves
# last_qrs_peak_num past them.
# Skip peaks a previous backsearch already rejected. A backsearch result
# depends only on qrs_thr and last_qrs_ind, which change only in
# _update_qrs, and _update_qrs moves last_qrs_peak_num to the current
# peak. If either of these stops being true, remove this skip.
start = max(self.last_qrs_peak_num, self._backsearch_peak_num) + 1
for peak_num in range(start, self.peak_num + 1):
if self._is_qrs(peak_num=peak_num, backsearch=True):
Expand Down
33 changes: 33 additions & 0 deletions tests/test_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,39 @@ def spy(peak_num, backsearch=False):
n_after = np.sum(xqrs.qrs_inds > 150 * fs)
assert n_before > 30 and n_after > 30

def test_xqrs_backsearch_skip_assumptions(self):
"""The backsearch skip relies on how XQRS updates its state (#597)."""
sig, fields = wfdb.rdsamp("sample-data/100", channels=[0])
fs = fields["fs"]
x = sig[:, 0]
rng = np.random.default_rng(0)
gap = 0.01 * rng.standard_normal(120 * fs)
# Scaled-down beats after the gap are only found by backsearch
y = np.concatenate([x[: 30 * fs], gap, 0.4 * x[30 * fs : 60 * fs]])

xqrs = processing.XQRS(sig=y, fs=fs)
update_noise = xqrs._update_noise
update_qrs = xqrs._update_qrs

def noise_spy(peak_num):
qrs_thr, last_qrs_ind = xqrs.qrs_thr, xqrs.last_qrs_ind
update_noise(peak_num)
# A noise update must not change the result of a backsearch
assert (xqrs.qrs_thr, xqrs.last_qrs_ind) == (qrs_thr, last_qrs_ind)

def qrs_spy(peak_num, backsearch=False):
update_qrs(peak_num, backsearch=backsearch)
# Peaks up to the current one count as already searched
assert xqrs.last_qrs_peak_num == xqrs.peak_num

with (
mock.patch.object(xqrs, "_update_noise", side_effect=noise_spy),
mock.patch.object(xqrs, "_update_qrs", side_effect=qrs_spy),
):
xqrs.detect(verbose=False)

assert len(xqrs.backsearch_qrs_inds) > 0


class TestXQRSDetect:
"""Tests for the xqrs_detect convenience wrapper."""
Expand Down
Loading