From af4958e7079022b23ea136f47e8c65b6299c9333 Mon Sep 17 00:00:00 2001 From: Bharat Kaurav <117659673+TheNetherWatcher@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:51:20 +0530 Subject: [PATCH] Keep invalid samples invalid in smooth_frames smooth_frames averaged the invalid sample value with the other samples in a frame, which gave a large valid-looking value instead of an invalid one (NaN in physical units). As in the WFDB library's meansamp(), a frame that contains an invalid sample now gives the invalid value. --- docs/changes.rst | 3 +++ src/wfdb/io/_signal.py | 15 ++++++++++----- tests/test_record.py | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 8c63c074..f55ea3a5 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -17,6 +17,9 @@ Unreleased **Add timeouts to requests for remote files** Requests for files on PhysioNet (and other web servers) now time out if the server does not respond (10 seconds to connect, and 60 seconds to wait for data), instead of waiting indefinitely. A ``requests.exceptions.Timeout`` (``ConnectTimeout`` or ``ReadTimeout``) is raised in this case. +**Fix smoothing of frames with invalid samples** + When a signal with more than one sample per frame is read with ``smooth_frames=True`` (the default), a frame that contains an invalid sample now gives an invalid value (NaN in physical units), as in the WFDB library. The invalid sample value used to be averaged with the other samples, which gave a large but valid-looking value. + **Build with uv_build** The package is now built with ``uv_build`` instead of ``hatchling``. The version number is specified in ``pyproject.toml`` only, and ``wfdb.__version__`` is read from the installed package metadata (it is ``"unknown"`` if the package is run from a source tree without being installed). diff --git a/src/wfdb/io/_signal.py b/src/wfdb/io/_signal.py index 7bdc5b60..2f340a8d 100644 --- a/src/wfdb/io/_signal.py +++ b/src/wfdb/io/_signal.py @@ -1060,15 +1060,20 @@ def smooth_frames(self, sigtype="physical"): if spf[ch] == 1: signal[:, ch] = expanded_signal[ch] else: + # As in the WFDB library, a frame with an invalid sample gives + # an invalid value. Physical samples are already NaN. + invalid = None + if sigtype == "digital" and self.fmt and self.fmt[ch]: + invalid = _digi_nan(self.fmt[ch]) frames = expanded_signal[ch].reshape(-1, spf[ch]) for chunk_start in range(0, sig_len, CHUNK_SIZE): chunk_end = chunk_start + CHUNK_SIZE - signal_sum = np.sum( - frames[chunk_start:chunk_end, :], - axis=1, - dtype=intermediate_dtype, - ) + chunk = frames[chunk_start:chunk_end, :] + signal_sum = np.sum(chunk, axis=1, dtype=intermediate_dtype) signal[chunk_start:chunk_end, ch] = signal_sum / spf[ch] + if invalid is not None: + has_invalid = np.any(chunk == invalid, axis=1) + signal[chunk_start:chunk_end, ch][has_invalid] = invalid return signal diff --git a/tests/test_record.py b/tests/test_record.py index 2bb636ae..efd7f722 100644 --- a/tests/test_record.py +++ b/tests/test_record.py @@ -857,6 +857,22 @@ def test_write_smoothed(self, temp_path): ) np.testing.assert_array_equal(record.d_signal, record2.d_signal) + def test_smooth_frames_invalid_sample(self, temp_path): + """A frame with an invalid sample is invalid after smoothing.""" + # Two samples per frame. -32768 is the invalid sample value for fmt 16. + raw = np.array([2, 4, -32768, 100, 6, 8], dtype="