Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.d/adc-all-nan-channel.rst
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 7 additions & 6 deletions src/wfdb/io/_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
34 changes: 34 additions & 0 deletions tests/test_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down