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."""