Skip to content

Commit cd242ef

Browse files
authored
Keep invalid samples invalid in smooth_frames (#608)
When a signal has more than one sample per frame, `smooth_frames` averages each frame in digital units. It doesn't check for the invalid sample value, so an invalid sample is averaged with the others and gives a large value that looks valid: ```python import numpy as np, wfdb # fmt 16, 2 samples per frame, gain 200. -32768 is the invalid sample value. np.array([0, 1, -1, -2, -32768, 100], dtype="<i2").tofile("r.dat") open("r.hea", "w").write("r 1 100 3\nr.dat 16x2 200/mV 16 0 0 0 0 sig\n") wfdb.rdrecord("r", smooth_frames=False).e_p_signal[0] # [ 0. 0.005 -0.005 -0.01 nan 0.5 ] wfdb.rdrecord("r").p_signal[:, 0] # [ 0. -0.005 -81.67] (last frame should be NaN) ``` The WFDB library returns an invalid value for any frame that contains an invalid sample (`meansamp()` in `lib/signal.c`), so this does the same: after averaging a chunk of frames in digital units, frames with an invalid sample are set to the invalid value for the channel's format. That then becomes NaN when converted to physical units. Physical smoothing (`sigtype="physical"`) already gave NaN, since NaN propagates through the sum. This doesn't change the rounding of valid frames, which is #335. Tests: a fmt 16 record with two samples per frame and one invalid sample reads back as `[3, -32768, 7]` (digital) and `[0.015, nan, 0.035]` (physical). This fails on `main`. `pytest -m "not network"` passes (97 tests), and the docs build passes.
2 parents 3f103d2 + af4958e commit cd242ef

3 files changed

Lines changed: 29 additions & 5 deletions

File tree

‎docs/changes.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Unreleased
1717
**Add timeouts to requests for remote files**
1818
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.
1919

20+
**Fix smoothing of frames with invalid samples**
21+
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.
22+
2023
**Build with uv_build**
2124
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).
2225

‎src/wfdb/io/_signal.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,15 +1060,20 @@ def smooth_frames(self, sigtype="physical"):
10601060
if spf[ch] == 1:
10611061
signal[:, ch] = expanded_signal[ch]
10621062
else:
1063+
# As in the WFDB library, a frame with an invalid sample gives
1064+
# an invalid value. Physical samples are already NaN.
1065+
invalid = None
1066+
if sigtype == "digital" and self.fmt and self.fmt[ch]:
1067+
invalid = _digi_nan(self.fmt[ch])
10631068
frames = expanded_signal[ch].reshape(-1, spf[ch])
10641069
for chunk_start in range(0, sig_len, CHUNK_SIZE):
10651070
chunk_end = chunk_start + CHUNK_SIZE
1066-
signal_sum = np.sum(
1067-
frames[chunk_start:chunk_end, :],
1068-
axis=1,
1069-
dtype=intermediate_dtype,
1070-
)
1071+
chunk = frames[chunk_start:chunk_end, :]
1072+
signal_sum = np.sum(chunk, axis=1, dtype=intermediate_dtype)
10711073
signal[chunk_start:chunk_end, ch] = signal_sum / spf[ch]
1074+
if invalid is not None:
1075+
has_invalid = np.any(chunk == invalid, axis=1)
1076+
signal[chunk_start:chunk_end, ch][has_invalid] = invalid
10721077

10731078
return signal
10741079

‎tests/test_record.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,22 @@ def test_write_smoothed(self, temp_path):
857857
)
858858
np.testing.assert_array_equal(record.d_signal, record2.d_signal)
859859

860+
def test_smooth_frames_invalid_sample(self, temp_path):
861+
"""A frame with an invalid sample is invalid after smoothing."""
862+
# Two samples per frame. -32768 is the invalid sample value for fmt 16.
863+
raw = np.array([2, 4, -32768, 100, 6, 8], dtype="<i2")
864+
raw.tofile(os.path.join(temp_path, "invalid.dat"))
865+
with open(os.path.join(temp_path, "invalid.hea"), "w") as f:
866+
f.write("invalid 1 100 3\n")
867+
f.write("invalid.dat 16x2 200/mV 16 0 0 0 0 sig\n")
868+
record_name = os.path.join(temp_path, "invalid")
869+
870+
record = wfdb.rdrecord(record_name, physical=False)
871+
np.testing.assert_array_equal(record.d_signal[:, 0], [3, -32768, 7])
872+
873+
record = wfdb.rdrecord(record_name)
874+
np.testing.assert_allclose(record.p_signal[:, 0], [0.015, np.nan, 0.035])
875+
860876
def test_to_dataframe(self):
861877
record = wfdb.rdrecord("sample-data/test01_00s")
862878
df = record.to_dataframe()

0 commit comments

Comments
 (0)