Skip to content

Commit 2f2a81f

Browse files
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.
1 parent b0b5b5f commit 2f2a81f

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
@@ -41,6 +41,9 @@ Unreleased
4141
**Fix resample_sig for signals starting with NaN**
4242
``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.
4343

44+
**Fix smoothing of frames with invalid samples**
45+
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.
46+
4447
Version 4.3.1 (Feb 2026)
4548
-----------------------------
4649

‎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)