diff --git a/docs/changes.rst b/docs/changes.rst index 060ca66a..ae676e6e 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -32,6 +32,9 @@ Unreleased **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). +**Fix resample_sig for signals starting with NaN** + ``resample_sig`` (and the functions that use it, such as ``resample_singlechan`` and ``resample_multichan``) returned only NaN values if the signal started with NaN. Leading NaN values now take the first valid value before resampling, as trailing ones already took the last. + Version 4.3.1 (Feb 2026) ----------------------------- diff --git a/src/wfdb/processing/basic.py b/src/wfdb/processing/basic.py index 576ccbb3..67ad8fdf 100644 --- a/src/wfdb/processing/basic.py +++ b/src/wfdb/processing/basic.py @@ -53,9 +53,10 @@ def resample_sig(x, fs, fs_target): return x, t new_length = int(x.shape[0] * fs_target / fs) - # Resample the array if NaN values are present + # Fill NaN values before resampling. Leading and trailing NaN values take + # the nearest valid value, since interpolate() leaves leading ones as NaN. if np.isnan(x).any(): - x = pd.Series(x.reshape((-1,))).interpolate().values + x = pd.Series(x.reshape((-1,))).interpolate(limit_direction="both").values resampled_x, resampled_t = signal.resample(x, num=new_length, t=t) assert resampled_x.shape == resampled_t.shape and resampled_x.shape[0] == new_length assert np.all(np.diff(resampled_t) > 0) diff --git a/tests/test_processing.py b/tests/test_processing.py index 2641b20b..c7d3c545 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -36,6 +36,27 @@ def test_resample_multi(self): assert new_sig.shape[0] == expected_length assert new_sig.shape[1] == sig.shape[1] + def test_resample_leading_nan(self): + """Leading NaN values take the first valid value before resampling.""" + x = np.array([np.nan, np.nan, 1.0, 2.0, 3.0, 4.0]) + filled = np.array([1.0, 1.0, 1.0, 2.0, 3.0, 4.0]) + + new_x, _ = processing.resample_sig(x, fs=2, fs_target=4) + expected, _ = processing.resample_sig(filled, fs=2, fs_target=4) + + assert np.isfinite(new_x).all() + np.testing.assert_allclose(new_x, expected) + + def test_resample_trailing_and_inner_nan(self): + """Inner NaN values are interpolated, and trailing ones take the last.""" + x = np.array([1.0, np.nan, 3.0, 4.0, np.nan, np.nan]) + filled = np.array([1.0, 2.0, 3.0, 4.0, 4.0, 4.0]) + + new_x, _ = processing.resample_sig(x, fs=2, fs_target=4) + expected, _ = processing.resample_sig(filled, fs=2, fs_target=4) + + np.testing.assert_allclose(new_x, expected) + def test_normalize_bound(self): sig, _ = wfdb.rdsamp("sample-data/100") lb = -5