From 36f1e2f5bd8aab97569c278d01b70aa955fb3f8a Mon Sep 17 00:00:00 2001 From: Bharat Kaurav <117659673+TheNetherWatcher@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:47:35 +0530 Subject: [PATCH 1/2] Use the last RR interval in compute_hr The loop stopped one interval early and copied the previous rate past the last beat, so the last RR interval was never used and two beats gave only NaN. --- docs/changes.rst | 3 +++ src/wfdb/processing/hr.py | 10 +++++----- tests/test_processing.py | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 060ca66a..07a89c58 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -29,6 +29,9 @@ Unreleased **Fix parameter handling in csv_to_wfdb** ``csv_to_wfdb`` now builds the same ``Record`` whether it writes files or returns the record (``record_only=True``), so all header parameters (such as ``dat_file_name``, ``counter_freq``, ``base_counter``, ``skew``, ``byte_offset``, ``adc_res`` and ``adc_zero``) are written instead of being ignored. Single values of any numeric type (including NumPy scalars and floats) are used for all channels, and an explicit ``0`` is no longer replaced by the default. The initial value and checksum are computed from the digital samples, and the default ADC resolution now follows ``fmt`` in both modes. The ``samps_per_frame``, ``init_value`` and ``checksum`` arguments are deprecated and ignored. The parameter descriptions in the docstring have been corrected. +**Use the last RR interval in compute_hr** + ``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. + **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). diff --git a/src/wfdb/processing/hr.py b/src/wfdb/processing/hr.py index 76750677..1aba0558 100644 --- a/src/wfdb/processing/hr.py +++ b/src/wfdb/processing/hr.py @@ -31,15 +31,15 @@ def compute_hr(sig_len, qrs_inds, fs): if len(qrs_inds) < 2: return heart_rate - for i in range(0, len(qrs_inds) - 2): + # The rate from each RR interval holds from just after its second beat up + # to the next beat, or to the end of the signal after the last one. + for i in range(0, len(qrs_inds) - 1): a = qrs_inds[i] b = qrs_inds[i + 1] - c = qrs_inds[i + 2] + end = qrs_inds[i + 2] + 1 if i + 2 < len(qrs_inds) else sig_len rr = (b - a) * (1.0 / fs) * 1000 hr = 60000.0 / rr - heart_rate[b + 1 : c + 1] = hr - - heart_rate[qrs_inds[-1] :] = heart_rate[qrs_inds[-1]] + heart_rate[b + 1 : end] = hr return heart_rate diff --git a/tests/test_processing.py b/tests/test_processing.py index 2641b20b..3921afdc 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -174,6 +174,28 @@ def test_correct_peaks(self): assert np.array_equal(yz, expected_peaks) +class TestComputeHr: + """Tests for compute_hr.""" + + def test_uses_last_rr_interval(self): + """The rate after the last beat comes from the last RR interval.""" + hr = processing.compute_hr(sig_len=14, qrs_inds=[0, 2, 6, 12], fs=60) + + expected = np.full(14, np.nan) + expected[3:7] = 60 * 60 / 2 + expected[7:13] = 60 * 60 / 4 + expected[13:] = 60 * 60 / 6 + np.testing.assert_allclose(hr, expected) + + def test_two_beats(self): + """Two beats give one RR interval, so the rate is known after them.""" + hr = processing.compute_hr(sig_len=10, qrs_inds=[2, 6], fs=1) + + expected = np.full(10, np.nan) + expected[7:] = 60 / 4 + np.testing.assert_allclose(hr, expected) + + class TestQrs: """Testing QRS detectors""" From b9e0542283fad11224a468d17c661075ef20c0c9 Mon Sep 17 00:00:00 2001 From: Bharat Kaurav <117659673+TheNetherWatcher@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:32:48 +0530 Subject: [PATCH 2/2] Document compute_hr intervals and test a last beat past the end --- src/wfdb/processing/hr.py | 4 ++++ tests/test_processing.py | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/src/wfdb/processing/hr.py b/src/wfdb/processing/hr.py index 1aba0558..4aba8510 100644 --- a/src/wfdb/processing/hr.py +++ b/src/wfdb/processing/hr.py @@ -9,6 +9,10 @@ def compute_hr(sig_len, qrs_inds, fs): """Compute instantaneous heart rate from peak indices. + The rate from each RR interval is assigned to the samples after its second + beat, up to and including the next beat. After the last beat, the rate + from the last RR interval is used up to the end of the signal. + Parameters ---------- sig_len : int diff --git a/tests/test_processing.py b/tests/test_processing.py index 3921afdc..78409d2e 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -195,6 +195,15 @@ def test_two_beats(self): expected[7:] = 60 / 4 np.testing.assert_allclose(hr, expected) + def test_last_beat_past_end(self): + """A last beat beyond the signal doesn't raise an IndexError.""" + hr = processing.compute_hr(sig_len=8, qrs_inds=[0, 2, 6, 12], fs=60) + + expected = np.full(8, np.nan) + expected[3:7] = 60 * 60 / 2 + expected[7:] = 60 * 60 / 4 + np.testing.assert_allclose(hr, expected) + class TestQrs: """Testing QRS detectors"""