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 @@ -80,6 +80,9 @@ Unreleased
**Use only beat annotations in ann2rr**
``ann2rr`` now computes intervals between beat (QRS) annotations only, as the WFDB ``ann2rr`` program does, so rhythm changes, comments and other non-beat annotations no longer split an RR interval in two. The first interval is still measured from the start of the record. To include all annotations as before, pass ``all_annotations=True`` (the equivalent of ``ann2rr -A``).

**Include the right edge of the window in find_local_peaks**
``find_local_peaks`` now compares each sample with ``radius`` samples on both sides, as documented, and is about 3 times faster. The sample ``radius`` positions to the right, and the last sample of the signal, used to be left out. ``XQRS`` uses ``find_local_peaks`` to find candidate peaks, so its detections may change slightly.

Version 4.3.1 (Feb 2026)
-----------------------------

Expand Down
21 changes: 5 additions & 16 deletions src/wfdb/processing/peaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ def find_local_peaks(sig, radius):

A sample is a local peak if it is the largest value within the <radius> samples on
its left and right. In cases where it shares the max value with nearby samples, the
middle sample is classified as the local peak.
first of these samples is classified as the local peak. The search then continues
<radius> samples later, so a flat top that is wider than <radius> samples gives a
peak every <radius> samples along it.

Parameters
----------
Expand All @@ -75,28 +77,15 @@ def find_local_peaks(sig, radius):

"""
# TODO: Fix flat mountain scenarios.
sig = np.asarray(sig)
if np.min(sig) == np.max(sig):
return np.empty(0)

peak_inds = []

i = 0
while i < radius + 1:
if sig[i] == max(sig[: i + radius]):
peak_inds.append(i)
i += radius
else:
i += 1

while i < len(sig):
if sig[i] == max(sig[i - radius : i + radius]):
peak_inds.append(i)
i += radius
else:
i += 1

while i < len(sig):
if sig[i] == max(sig[i - radius :]):
if sig[i] == sig[max(0, i - radius) : i + radius + 1].max():
peak_inds.append(i)
i += radius
else:
Expand Down
11 changes: 11 additions & 0 deletions tests/test_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ def test_find_peaks_empty(self):
assert hp.shape == (0,)
assert sp.shape == (0,)

def test_find_local_peaks(self):
# the sample `radius` positions to the right is part of the window
x = np.array([0, 1, 2, 3, 2, 1, 0], dtype=float)
assert np.array_equal(processing.find_local_peaks(x, 1), [3])
x = np.array([0, 1, 3, 2, 10, 5, 1, 0], dtype=float)
assert np.array_equal(processing.find_local_peaks(x, 2), [4])

def test_find_local_peaks_list(self):
"""A list works as well as an array."""
assert processing.find_local_peaks([0, 1, 2, 3, 2, 1, 0], 1).tolist() == [3]

def test_gqrs(self):
record = wfdb.rdrecord(
"sample-data/100",
Expand Down
Loading