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
3 changes: 3 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Unreleased
**Check that QRS indices are increasing in compute_hr**
``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.

**Fix rr2ann with times and with a symbol column**
``rr2ann`` with ``as_time=True`` now rounds times to the nearest sample instead of truncating them, so (for example) 0.29 seconds at 100 Hz gives sample 29 instead of 28. Intervals in samples with a second column of symbols no longer raise an error, and fractional intervals are summed before rounding.

**Use Ruff for formatting and linting**
The ``dev`` extra now includes ``ruff`` instead of ``black`` and ``pylint``, and the code is formatted with a line length of 88 (the default).

Expand Down
6 changes: 4 additions & 2 deletions src/wfdb/processing/hr.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,12 @@ def rr2ann(rr_array, record_name, extension, fs=250, as_time=False):
except IndexError:
ann_sample = rr_array

# With a symbol column, the array holds strings, so convert the numbers.
if as_time:
ann_sample = (fs * ann_sample.astype(np.float64)).astype(np.int64)
ann_sample = np.round(fs * ann_sample.astype(np.float64)).astype(np.int64)
else:
ann_sample = np.cumsum(ann_sample).astype(np.int64)
# Sum as floats and round once, so fractional intervals don't drift.
ann_sample = np.round(np.cumsum(ann_sample.astype(np.float64))).astype(np.int64)

try:
ann_symbol = rr_array[:, 1].tolist()
Expand Down
43 changes: 43 additions & 0 deletions tests/test_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,46 @@ def fake_detect(self, *args, **kwargs):

assert mock_detect.call_count == 1
assert mock_detect.call_args.kwargs["learn"] is False


class TestRr2Ann:
"""Tests for rr2ann."""

def test_times_are_rounded(self, tmp_path, monkeypatch):
"""Times are rounded to the nearest sample, not truncated."""
monkeypatch.chdir(tmp_path)
times = np.array([0.29, 0.57, 1.13])
processing.hr.rr2ann(times, "t", "atr", fs=100, as_time=True)

ann = wfdb.rdann("t", "atr")
np.testing.assert_array_equal(ann.sample, [29, 57, 113])

def test_intervals_with_symbols(self, tmp_path, monkeypatch):
"""Intervals with a column of symbols are summed into samples."""
monkeypatch.chdir(tmp_path)
rr_array = np.array([[4, 17, 18], ["V", "N", "V"]]).T
processing.hr.rr2ann(rr_array, "t", "atr")

ann = wfdb.rdann("t", "atr")
np.testing.assert_array_equal(ann.sample, [4, 21, 39])
assert ann.symbol == ["V", "N", "V"]

def test_times_with_symbols(self, tmp_path, monkeypatch):
"""Times with a column of symbols, as in the docstring example."""
monkeypatch.chdir(tmp_path)
rr_array = np.array([[0.2, 0.6, 1.3], ["V", "N", "V"]]).T
processing.hr.rr2ann(rr_array, "t", "atr", fs=100, as_time=True)

ann = wfdb.rdann("t", "atr")
np.testing.assert_array_equal(ann.sample, [20, 60, 130])
assert ann.symbol == ["V", "N", "V"]

def test_fractional_intervals(self, tmp_path, monkeypatch):
"""Fractional intervals are summed before rounding, so they don't drift."""
monkeypatch.chdir(tmp_path)
processing.hr.rr2ann(np.array([4.5, 4.5, 4.5]), "t", "atr")
np.testing.assert_array_equal(wfdb.rdann("t", "atr").sample, [4, 9, 14])

rr_array = np.array([[4.5, 4.5, 4.5], ["V", "N", "V"]]).T
processing.hr.rr2ann(rr_array, "t", "atr")
np.testing.assert_array_equal(wfdb.rdann("t", "atr").sample, [4, 9, 14])
Loading