Skip to content

Commit 2cdbd2a

Browse files
authored
Check that QRS indices are increasing in compute_hr (#603)
Fixes #602. `compute_hr` now raises a `ValueError` if `qrs_inds` is not strictly increasing, naming the first pair that isn't, for example: ``` ValueError: qrs_inds must be strictly increasing, but qrs_inds[1] = 8 is followed by qrs_inds[2] = 8 ``` Before, repeated beats gave a `ZeroDivisionError` (lists) or `inf` with a `RuntimeWarning` (arrays), and beats out of order gave a negative heart rate. The docstring now says the indices must be strictly increasing and documents the error. Tests: repeated, decreasing and repeated-last-beat inputs (lists and arrays) raise the error. `pytest -m "not network"` passes (103 tests), and the docs build passes.
2 parents 577a18b + 4c58440 commit 2cdbd2a

3 files changed

Lines changed: 37 additions & 1 deletion

File tree

‎docs/changes.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ Unreleased
3232
**Use the last RR interval in compute_hr**
3333
``compute_hr`` now uses the interval between the last two beats for the heart rate after the last beat, instead of repeating the previous rate. With exactly two beats it now returns their rate after the second beat instead of only NaN.
3434

35+
**Check that QRS indices are increasing in compute_hr**
36+
``compute_hr`` now raises a ``ValueError`` if ``qrs_inds`` is not strictly increasing. Repeated beats used to give a ``ZeroDivisionError`` (or ``inf`` for NumPy arrays), and beats out of order gave a negative heart rate.
37+
3538
**Use Ruff for formatting and linting**
3639
The ``dev`` extra now includes ``ruff`` instead of ``black`` and ``pylint``, and the code is formatted with a line length of 88 (the default).
3740

‎src/wfdb/processing/hr.py‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def compute_hr(sig_len, qrs_inds, fs):
1818
sig_len : int
1919
The length of the corresponding signal.
2020
qrs_inds : ndarray
21-
The QRS index locations.
21+
The QRS index locations, in strictly increasing order.
2222
fs : int, float
2323
The corresponding signal's sampling frequency.
2424
@@ -29,7 +29,22 @@ def compute_hr(sig_len, qrs_inds, fs):
2929
corresponding signal. Contains numpy.nan where heart rate could
3030
not be computed.
3131
32+
Raises
33+
------
34+
ValueError
35+
If `qrs_inds` is not strictly increasing.
36+
3237
"""
38+
# Compare neighbours directly, since np.diff wraps for unsigned integers.
39+
q = np.asarray(qrs_inds)
40+
not_increasing = np.flatnonzero(q[1:] <= q[:-1])
41+
if not_increasing.size:
42+
i = not_increasing[0]
43+
raise ValueError(
44+
"qrs_inds must be strictly increasing, but "
45+
f"qrs_inds[{i}] = {q[i]} is followed by qrs_inds[{i + 1}] = {q[i + 1]}"
46+
)
47+
3348
heart_rate = np.full(sig_len, np.nan, dtype="float32")
3449

3550
if len(qrs_inds) < 2:

‎tests/test_processing.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from unittest import mock
22

33
import numpy as np
4+
import pytest
45

56
import wfdb
67
from wfdb import processing
@@ -204,6 +205,23 @@ def test_last_beat_past_end(self):
204205
expected[7:] = 60 * 60 / 4
205206
np.testing.assert_allclose(hr, expected)
206207

208+
@pytest.mark.parametrize("qrs_inds", [[2, 8, 8, 15], [2, 8, 5, 15], [2, 8, 8]])
209+
def test_rejects_unordered_indices(self, qrs_inds):
210+
"""Repeated or decreasing beats raise a ValueError."""
211+
with pytest.raises(ValueError, match="strictly increasing"):
212+
processing.compute_hr(sig_len=20, qrs_inds=qrs_inds, fs=1)
213+
214+
def test_rejects_unordered_array(self):
215+
"""The check also applies to NumPy arrays."""
216+
with pytest.raises(ValueError, match=r"qrs_inds\[1\] = 8"):
217+
processing.compute_hr(sig_len=20, qrs_inds=np.array([2, 8, 8]), fs=1)
218+
219+
def test_rejects_unordered_unsigned(self):
220+
"""Decreasing unsigned indices are caught, although np.diff wraps."""
221+
qrs_inds = np.array([2, 8, 5], dtype=np.uint32)
222+
with pytest.raises(ValueError, match=r"qrs_inds\[1\] = 8"):
223+
processing.compute_hr(sig_len=20, qrs_inds=qrs_inds, fs=1)
224+
207225

208226
class TestQrs:
209227
"""Testing QRS detectors"""

0 commit comments

Comments
 (0)