Skip to content

pantompkins

biosigpy.ecg.pantompkins.pantompkins

pantompkins(ecg: ArrayLike, sampling_frequency: float, *, bandpass_frequency: ArrayLike = (5.0, 12.0), integration_window_size: float = 0.15, minimum_peak_distance: float = 0.5, snap_to_peak_window_size: float = 20.0) -> dict[str, np.ndarray]

Detect ECG R waves with the Biosiglib Pan-Tompkins workflow.

Parameters:

Name Type Description Default
ecg array_like

One-dimensional real numeric ECG signal with at least two samples. Infinite values are invalid.

required
sampling_frequency float

Positive sampling frequency in Hz.

required
bandpass_frequency array_like

Two-element bandpass frequency vector in Hz. Values must be positive, strictly increasing, and inside the Nyquist range.

(5.0, 12.0)
integration_window_size float

Moving-integration window length in seconds.

0.15
minimum_peak_distance float

Minimum distance between envelope peaks in seconds.

0.5
snap_to_peak_window_size float

Search radius in samples used when refining detections to local ECG maxima.

20.0

Returns:

Type Description
dict[str, ndarray]

Dictionary with r_wave_times in seconds, ecg_filtered, decg_squared, and decg_envelope.

Raises:

Type Description
TypeError

If numeric inputs are not real numeric data.

ValueError

If inputs are outside the accepted shape, range, or positivity constraints.

Notes

This function follows the Biosiglib ecg.pantompkins specification. It returns R-wave times and intermediate processing signals. Bandpass and derivative filtering use the NaN-aware public filtering tools with max_gap=0. Peak refinement uses :func:biosigpy.tools.snap_to_peak.snap_to_peak, and derivative filter design uses :func:biosigpy.tools.lpd_filter.lpd_filter.

Examples:

>>> import numpy as np
>>> from biosigpy.ecg import pantompkins
>>> fs = 256.0
>>> time = np.arange(0.0, 2.0, 1.0 / fs)
>>> ecg = np.sin(2.0 * np.pi * 1.2 * time)
>>> outputs = pantompkins(ecg, fs)
>>> "r_wave_times" in outputs
True
Source code in src/biosigpy/ecg/pantompkins.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def pantompkins(
    ecg: ArrayLike,
    sampling_frequency: float,
    *,
    bandpass_frequency: ArrayLike = (5.0, 12.0),
    integration_window_size: float = 0.15,
    minimum_peak_distance: float = 0.5,
    snap_to_peak_window_size: float = 20.0,
) -> dict[str, np.ndarray]:
    """Detect ECG R waves with the Biosiglib Pan-Tompkins workflow.

    Parameters
    ----------
    ecg : array_like
        One-dimensional real numeric ECG signal with at least two samples.
        Infinite values are invalid.
    sampling_frequency : float
        Positive sampling frequency in Hz.
    bandpass_frequency : array_like, optional
        Two-element bandpass frequency vector in Hz. Values must be positive,
        strictly increasing, and inside the Nyquist range.
    integration_window_size : float, optional
        Moving-integration window length in seconds.
    minimum_peak_distance : float, optional
        Minimum distance between envelope peaks in seconds.
    snap_to_peak_window_size : float, optional
        Search radius in samples used when refining detections to local ECG
        maxima.

    Returns
    -------
    dict[str, numpy.ndarray]
        Dictionary with ``r_wave_times`` in seconds, ``ecg_filtered``,
        ``decg_squared``, and ``decg_envelope``.

    Raises
    ------
    TypeError
        If numeric inputs are not real numeric data.
    ValueError
        If inputs are outside the accepted shape, range, or positivity
        constraints.

    Notes
    -----
    This function follows the Biosiglib ``ecg.pantompkins`` specification. It
    returns R-wave times and intermediate processing signals. Bandpass and
    derivative filtering use the NaN-aware public filtering tools with
    ``max_gap=0``. Peak refinement uses
    :func:`biosigpy.tools.snap_to_peak.snap_to_peak`, and derivative filter
    design uses :func:`biosigpy.tools.lpd_filter.lpd_filter`.

    Examples
    --------
    >>> import numpy as np
    >>> from biosigpy.ecg import pantompkins
    >>> fs = 256.0
    >>> time = np.arange(0.0, 2.0, 1.0 / fs)
    >>> ecg = np.sin(2.0 * np.pi * 1.2 * time)
    >>> outputs = pantompkins(ecg, fs)
    >>> "r_wave_times" in outputs
    True
    """

    ecg_vector = _ecg_vector(ecg)
    fs = as_positive_real_scalar(sampling_frequency, name="sampling_frequency")
    bandpass = _bandpass_pair(bandpass_frequency, fs)
    window_seconds = as_positive_real_scalar(
        integration_window_size, name="integration_window_size"
    )
    peak_distance_seconds = as_positive_real_scalar(
        minimum_peak_distance, name="minimum_peak_distance"
    )
    snap_to_peak_window_size = as_positive_real_scalar(
        snap_to_peak_window_size, name="snap_to_peak_window_size"
    )

    b_bandpass, a_bandpass = signal.butter(
        4, bandpass, btype="bandpass", fs=fs
    )
    ecg_filtered = nan_filtfilt(
        b_bandpass,
        a_bandpass,
        ecg_vector,
        max_gap=0,
    )

    derivative_filter, _ = lpd_filter(fs, bandpass[1], order=4)
    decg = nan_filter(
        derivative_filter,
        [1.0],
        ecg_filtered,
        max_gap=0,
    )
    decg_squared = decg**2

    window_samples = int(np.floor(fs * window_seconds + 0.5))
    if window_samples < 1:
        raise ValueError("integration_window_size is shorter than one sample")
    integration_kernel = np.ones(window_samples, dtype=np.float64) / window_samples
    decg_envelope = np.convolve(decg_squared, integration_kernel, mode="same")

    minimum_distance_samples = int(np.floor(fs * peak_distance_seconds + 0.5))
    peak_indices, _ = signal.find_peaks(
        decg_envelope, distance=max(1, minimum_distance_samples)
    )
    peak_indices = np.unique(np.sort(peak_indices))
    filter_edge_margin = 3 * max(b_bandpass.size, a_bandpass.size)
    peak_indices = snap_to_peak(
        ecg_vector, peak_indices.astype(np.float64) + 1.0, snap_to_peak_window_size
    )
    peak_indices = peak_indices[np.isfinite(peak_indices)] - 1.0
    peak_indices = peak_indices[peak_indices >= filter_edge_margin]

    return {
        "r_wave_times": peak_indices.astype(np.float64) / fs,
        "ecg_filtered": np.asarray(ecg_filtered, dtype=np.float64),
        "decg_squared": np.asarray(decg_squared, dtype=np.float64),
        "decg_envelope": np.asarray(decg_envelope, dtype=np.float64),
    }