From 3340b538a8869eed99257c3333676b1f52198514 Mon Sep 17 00:00:00 2001 From: Roman Claudiu Date: Sun, 8 Nov 2026 11:26:10 +0200 Subject: [PATCH] Handle all-NaN channels in calc_adc_params The all-NaN guard in calc_adc_gain_baseline compared pmin == np.nan, which is always False, so a channel containing only NaN values fell through to the regular case and failed with "cannot convert float NaN to integer". Use np.isnan instead. Compute the channel minimum and maximum with np.fmin/np.fmax.reduce, which ignore NaN like nanmin/nanmax but do not warn about all-NaN channels. --- changelog.d/adc-all-nan-channel.rst | 2 ++ src/wfdb/io/_signal.py | 13 ++++++----- tests/test_record.py | 34 +++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 changelog.d/adc-all-nan-channel.rst diff --git a/changelog.d/adc-all-nan-channel.rst b/changelog.d/adc-all-nan-channel.rst new file mode 100644 index 00000000..2517d9a1 --- /dev/null +++ b/changelog.d/adc-all-nan-channel.rst @@ -0,0 +1,2 @@ +**Fix writing channels that are entirely NaN** + ``wrsamp`` and ``calc_adc_params`` no longer fail with ``cannot convert float NaN to integer`` when a channel contains only NaN values, and no longer emit an "All-NaN slice encountered" warning for such channels. diff --git a/src/wfdb/io/_signal.py b/src/wfdb/io/_signal.py index 2b671c31..0a7e02f2 100644 --- a/src/wfdb/io/_signal.py +++ b/src/wfdb/io/_signal.py @@ -748,7 +748,7 @@ def calc_adc_gain_baseline(self, ch, minvals, maxvals): # Figure out digital samples used to store physical samples # If the entire signal is NAN, gain/baseline won't be used - if pmin == np.nan: + if np.isnan(pmin): adc_gain = 1 baseline = 1 # If the signal is just one value, store one digital value. @@ -829,9 +829,10 @@ def calc_adc_params(self): raise ValueError("Signal contains inf. Cannot perform adc.") # min and max ignoring nans, unless whole channel is NAN. - # copy as 64-bit float to avoid precision loss - minvals = np.nanmin(self.p_signal, axis=0).astype("float64") - maxvals = np.nanmax(self.p_signal, axis=0).astype("float64") + # fmin/fmax behave like nanmin/nanmax without warning about + # all-NaN channels. Copy as 64-bit float to avoid precision loss. + minvals = np.fmin.reduce(self.p_signal, axis=0).astype("float64") + maxvals = np.fmax.reduce(self.p_signal, axis=0).astype("float64") for ch in range(np.shape(self.p_signal)[1]): adc_gain, baseline = self.calc_adc_gain_baseline(ch, minvals, maxvals) @@ -843,8 +844,8 @@ def calc_adc_params(self): maxvals = [] for ch in self.e_p_signal: # Copy as float for precise arithmetic - minvals.append(float(np.nanmin(ch))) - maxvals.append(float(np.nanmax(ch))) + minvals.append(float(np.fmin.reduce(ch))) + maxvals.append(float(np.fmax.reduce(ch))) if any(x == math.inf for x in minvals) or any( x == math.inf for x in maxvals diff --git a/tests/test_record.py b/tests/test_record.py index 788a9abe..72b1b043 100644 --- a/tests/test_record.py +++ b/tests/test_record.py @@ -1615,6 +1615,40 @@ def test_adc_gain_min_boundary(self, temp_path): expected_gain = (32767 - (-2147483648)) / (base_value + 1.5) np.testing.assert_allclose(record.adc_gain[0], expected_gain, rtol=1e-3) + @pytest.mark.filterwarnings("error::RuntimeWarning") + def test_all_nan_channel(self, temp_path): + """Write and read back a signal with a channel that is entirely NaN.""" + p_signal = np.array( + [ + [np.nan, 0.5], + [np.nan, -0.25], + [np.nan, np.nan], + ] + ) + + record = wfdb.Record(p_signal=p_signal, fmt=["16", "16"]) + adc_gain, baseline = record.calc_adc_params() + assert adc_gain[0] == 1 + assert baseline[0] == 1 + + record = wfdb.Record(e_p_signal=list(p_signal.transpose()), fmt=["16", "16"]) + assert record.calc_adc_params() == (adc_gain, baseline) + + wfdb.wrsamp( + "test_all_nan_channel", + fs=250, + sig_name=["I", "II"], + units=["mV", "mV"], + p_signal=p_signal, + fmt=["16", "16"], + write_dir=temp_path, + ) + record = wfdb.rdrecord(os.path.join(temp_path, "test_all_nan_channel")) + assert np.isnan(record.p_signal[:, 0]).all() + np.testing.assert_allclose( + record.p_signal[:, 1], p_signal[:, 1], atol=1 / record.adc_gain[1] + ) + class TestSigCohesion: """Tests for the digital range check in check_sig_cohesion."""