Skip to content

Commit 3340b53

Browse files
committed
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.
1 parent 263624c commit 3340b53

3 files changed

Lines changed: 43 additions & 6 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
**Fix writing channels that are entirely NaN**
2+
``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.

‎src/wfdb/io/_signal.py‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ def calc_adc_gain_baseline(self, ch, minvals, maxvals):
748748
# Figure out digital samples used to store physical samples
749749

750750
# If the entire signal is NAN, gain/baseline won't be used
751-
if pmin == np.nan:
751+
if np.isnan(pmin):
752752
adc_gain = 1
753753
baseline = 1
754754
# If the signal is just one value, store one digital value.
@@ -829,9 +829,10 @@ def calc_adc_params(self):
829829
raise ValueError("Signal contains inf. Cannot perform adc.")
830830

831831
# min and max ignoring nans, unless whole channel is NAN.
832-
# copy as 64-bit float to avoid precision loss
833-
minvals = np.nanmin(self.p_signal, axis=0).astype("float64")
834-
maxvals = np.nanmax(self.p_signal, axis=0).astype("float64")
832+
# fmin/fmax behave like nanmin/nanmax without warning about
833+
# all-NaN channels. Copy as 64-bit float to avoid precision loss.
834+
minvals = np.fmin.reduce(self.p_signal, axis=0).astype("float64")
835+
maxvals = np.fmax.reduce(self.p_signal, axis=0).astype("float64")
835836

836837
for ch in range(np.shape(self.p_signal)[1]):
837838
adc_gain, baseline = self.calc_adc_gain_baseline(ch, minvals, maxvals)
@@ -843,8 +844,8 @@ def calc_adc_params(self):
843844
maxvals = []
844845
for ch in self.e_p_signal:
845846
# Copy as float for precise arithmetic
846-
minvals.append(float(np.nanmin(ch)))
847-
maxvals.append(float(np.nanmax(ch)))
847+
minvals.append(float(np.fmin.reduce(ch)))
848+
maxvals.append(float(np.fmax.reduce(ch)))
848849

849850
if any(x == math.inf for x in minvals) or any(
850851
x == math.inf for x in maxvals

‎tests/test_record.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1615,6 +1615,40 @@ def test_adc_gain_min_boundary(self, temp_path):
16151615
expected_gain = (32767 - (-2147483648)) / (base_value + 1.5)
16161616
np.testing.assert_allclose(record.adc_gain[0], expected_gain, rtol=1e-3)
16171617

1618+
@pytest.mark.filterwarnings("error::RuntimeWarning")
1619+
def test_all_nan_channel(self, temp_path):
1620+
"""Write and read back a signal with a channel that is entirely NaN."""
1621+
p_signal = np.array(
1622+
[
1623+
[np.nan, 0.5],
1624+
[np.nan, -0.25],
1625+
[np.nan, np.nan],
1626+
]
1627+
)
1628+
1629+
record = wfdb.Record(p_signal=p_signal, fmt=["16", "16"])
1630+
adc_gain, baseline = record.calc_adc_params()
1631+
assert adc_gain[0] == 1
1632+
assert baseline[0] == 1
1633+
1634+
record = wfdb.Record(e_p_signal=list(p_signal.transpose()), fmt=["16", "16"])
1635+
assert record.calc_adc_params() == (adc_gain, baseline)
1636+
1637+
wfdb.wrsamp(
1638+
"test_all_nan_channel",
1639+
fs=250,
1640+
sig_name=["I", "II"],
1641+
units=["mV", "mV"],
1642+
p_signal=p_signal,
1643+
fmt=["16", "16"],
1644+
write_dir=temp_path,
1645+
)
1646+
record = wfdb.rdrecord(os.path.join(temp_path, "test_all_nan_channel"))
1647+
assert np.isnan(record.p_signal[:, 0]).all()
1648+
np.testing.assert_allclose(
1649+
record.p_signal[:, 1], p_signal[:, 1], atol=1 / record.adc_gain[1]
1650+
)
1651+
16181652

16191653
class TestSigCohesion:
16201654
"""Tests for the digital range check in check_sig_cohesion."""

0 commit comments

Comments
 (0)