Source code for ecgdatakit.processing.resample
"""ECG signal resampling utilities."""
from __future__ import annotations
import math
from fractions import Fraction
import numpy as np
from ecgdatakit.models import Lead, LeadLike
from ecgdatakit.processing._core import ensure_lead, new_lead, require_finite, require_scipy
[docs]
def resample(lead: LeadLike, target_rate: int, *, fs: int | None = None) -> Lead:
"""Resample a lead to a different sample rate.
Uses polyphase rational resampling (``scipy.signal.resample_poly``)
which preserves signal content up to the Nyquist frequency of the
lower sample rate. The signal is extended linearly at both ends
(``padtype="line"``) so a DC level or slow drift does not ring at the
edges. Output length is ``ceil(n * target_rate / fs)``.
Parameters
----------
lead : Lead | NDArray[np.float64]
Input ECG lead or 1-D signal array (must be finite).
target_rate : int
Desired output sample rate in Hz. A whole-number float such as
``250.0`` is accepted; the result is stored as an int.
fs : int | None
Sample rate in Hz. Required when *lead* is a numpy array.
"""
lead = ensure_lead(lead, fs=fs)
try:
valid = math.isfinite(target_rate) and target_rate > 0
except TypeError:
valid = False
if not valid:
raise ValueError(f"target_rate must be positive, got {target_rate!r}")
if target_rate != int(target_rate):
raise ValueError(
f"target_rate must be a whole number of Hz, got {target_rate}"
)
target_rate = int(target_rate)
require_finite(lead, "resample")
if target_rate == lead.sampling_rate:
return new_lead(lead, samples=lead.samples.copy(), sampling_rate=target_rate)
sig = require_scipy("signal")
# Exact ratio, also for a non-integer source rate (e.g. 499.5 Hz)
ratio = Fraction(target_rate) / Fraction(lead.sampling_rate).limit_denominator(10**6)
up, down = ratio.numerator, ratio.denominator
resampled = sig.resample_poly(lead.samples, up, down, padtype="line").astype(np.float64)
return new_lead(lead, samples=resampled, sampling_rate=target_rate)