Skip to content

Commit 34b989e

Browse files
authored
Fix NaN handling in Record.adc, and other fixes (#481)
Fix several bugs in `Record.adc`: 1. Previously, the function would try to convert all samples to integers and then, for any samples that were NaN, replace the corresponding elements with the appropriate sentinel value. Even though this was probably safe in most cases, casting NaN to an integer is implementation-defined behavior, and raises a warning by default (issue #480). 2. NaN just plain wasn't handled for the `inplace=True, expanded=False` case. (Currently, we don't use `inplace=True` anywhere internally; although it saves a bit of memory, it's destructive and so it's probably wise for high-level functions like `wrsamp` to avoid it.) 3. The `expanded=True` case relied on `self.n_sig` (in contrast to `expanded=False`, which operates based on the dimensions of `p_signal`.) This meant it would fail if the caller didn't explicitly set `n_sig`, which was an annoying inconsistency. Also, tidy up duplicated code and make things a little more efficient. A side note: I don't think the `inplace=True` mode is particularly great to have. It conflates two things (modifying the Record object attributes, which many applications want; and modifying the array contents, which you may think you want until you realize it subtly breaks something.) It does save some memory, but not as much as you'd hope. (That `copy=False` is pretty much a lie.) And of course I don't like functions whose return type is dependent on their arguments. So I would definitely put `inplace` on the chopping block for 5.0.0. Still, I think the updated code here isn't too terribly ugly. This set of changes is the first step to making `wfdb.wrsamp` work for multi-frequency (issue #336). Next is to fix `Record.calc_adc_params`, then `Record.set_d_features`.
2 parents 0130d70 + 5b56407 commit 34b989e

File tree

2 files changed

+42
-50
lines changed

2 files changed

+42
-50
lines changed

Diff for: tests/test_record.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1053,19 +1053,20 @@ def test_physical_conversion(self):
10531053
adc_gain = [1.0, 1234.567, 765.4321]
10541054
baseline = [10, 20, -30]
10551055
d_signal = np.repeat(np.arange(-100, 100), 3).reshape(-1, 3)
1056+
d_signal[5:10, :] = [-32768, -2048, -128]
10561057
e_d_signal = list(d_signal.transpose())
1057-
fmt = ["16", "16", "16"]
1058+
fmt = ["16", "212", "80"]
10581059

10591060
# Test adding or subtracting a small offset (0.01 ADU) to check
10601061
# that we correctly round to the nearest integer
10611062
for offset in (0, -0.01, 0.01):
10621063
p_signal = (d_signal + offset - baseline) / adc_gain
1064+
p_signal[5:10, :] = np.nan
10631065
e_p_signal = list(p_signal.transpose())
10641066

10651067
# Test converting p_signal to d_signal
10661068

10671069
record = wfdb.Record(
1068-
n_sig=n_sig,
10691070
p_signal=p_signal.copy(),
10701071
adc_gain=adc_gain,
10711072
baseline=baseline,
@@ -1081,7 +1082,6 @@ def test_physical_conversion(self):
10811082
# Test converting e_p_signal to e_d_signal
10821083

10831084
record = wfdb.Record(
1084-
n_sig=n_sig,
10851085
e_p_signal=[s.copy() for s in e_p_signal],
10861086
adc_gain=adc_gain,
10871087
baseline=baseline,
@@ -1108,7 +1108,7 @@ def test_physical_conversion(self):
11081108
p_signal=p_signal,
11091109
adc_gain=adc_gain,
11101110
baseline=baseline,
1111-
fmt=["16", "16", "16"],
1111+
fmt=fmt,
11121112
write_dir=self.temp_path,
11131113
)
11141114
record = wfdb.rdrecord(

Diff for: wfdb/io/_signal.py

+38-46
Original file line numberDiff line numberDiff line change
@@ -532,68 +532,60 @@ def adc(self, expanded=False, inplace=False):
532532
# To do: choose the minimum return res needed
533533
intdtype = "int64"
534534

535+
# Convert a physical (1D or 2D) signal array to digital. Note that
536+
# the input array is modified!
537+
def adc_inplace(p_signal, adc_gain, baseline, d_nan):
538+
nanlocs = np.isnan(p_signal)
539+
np.multiply(p_signal, adc_gain, p_signal)
540+
np.add(p_signal, baseline, p_signal)
541+
np.round(p_signal, 0, p_signal)
542+
np.copyto(p_signal, d_nan, where=nanlocs)
543+
d_signal = p_signal.astype(intdtype, copy=False)
544+
return d_signal
545+
535546
# Do inplace conversion and set relevant variables.
536547
if inplace:
537548
if expanded:
538-
for ch in range(self.n_sig):
539-
# NAN locations for the channel
540-
ch_nanlocs = np.isnan(self.e_p_signal[ch])
541-
np.multiply(
542-
self.e_p_signal[ch],
549+
for ch, ch_p_signal in enumerate(self.e_p_signal):
550+
ch_d_signal = adc_inplace(
551+
ch_p_signal,
543552
self.adc_gain[ch],
544-
self.e_p_signal[ch],
545-
)
546-
np.add(
547-
self.e_p_signal[ch],
548553
self.baseline[ch],
549-
self.e_p_signal[ch],
550-
)
551-
np.round(self.e_p_signal[ch], 0, self.e_p_signal[ch])
552-
self.e_p_signal[ch] = self.e_p_signal[ch].astype(
553-
intdtype, copy=False
554+
d_nans[ch],
554555
)
555-
self.e_p_signal[ch][ch_nanlocs] = d_nans[ch]
556+
self.e_p_signal[ch] = ch_d_signal
556557
self.e_d_signal = self.e_p_signal
557558
self.e_p_signal = None
558559
else:
559-
nanlocs = np.isnan(self.p_signal)
560-
np.multiply(self.p_signal, self.adc_gain, self.p_signal)
561-
np.add(self.p_signal, self.baseline, self.p_signal)
562-
np.round(self.p_signal, 0, self.p_signal)
563-
self.p_signal = self.p_signal.astype(intdtype, copy=False)
564-
self.d_signal = self.p_signal
560+
self.d_signal = adc_inplace(
561+
self.p_signal,
562+
self.adc_gain,
563+
self.baseline,
564+
d_nans,
565+
)
565566
self.p_signal = None
566567

567568
# Return the variable
568569
else:
569570
if expanded:
570-
d_signal = []
571-
for ch in range(self.n_sig):
572-
# NAN locations for the channel
573-
ch_nanlocs = np.isnan(self.e_p_signal[ch])
574-
ch_d_signal = self.e_p_signal[ch].copy()
575-
np.multiply(ch_d_signal, self.adc_gain[ch], ch_d_signal)
576-
np.add(ch_d_signal, self.baseline[ch], ch_d_signal)
577-
np.round(ch_d_signal, 0, ch_d_signal)
578-
ch_d_signal = ch_d_signal.astype(intdtype, copy=False)
579-
ch_d_signal[ch_nanlocs] = d_nans[ch]
580-
d_signal.append(ch_d_signal)
571+
e_d_signal = []
572+
for ch, ch_p_signal in enumerate(self.e_p_signal):
573+
ch_d_signal = adc_inplace(
574+
ch_p_signal.copy(),
575+
self.adc_gain[ch],
576+
self.baseline[ch],
577+
d_nans[ch],
578+
)
579+
e_d_signal.append(ch_d_signal)
580+
return e_d_signal
581581

582582
else:
583-
nanlocs = np.isnan(self.p_signal)
584-
# Cannot cast dtype to int now because gain is float.
585-
d_signal = self.p_signal.copy()
586-
np.multiply(d_signal, self.adc_gain, d_signal)
587-
np.add(d_signal, self.baseline, d_signal)
588-
np.round(d_signal, 0, d_signal)
589-
d_signal = d_signal.astype(intdtype, copy=False)
590-
591-
if nanlocs.any():
592-
for ch in range(d_signal.shape[1]):
593-
if nanlocs[:, ch].any():
594-
d_signal[nanlocs[:, ch], ch] = d_nans[ch]
595-
596-
return d_signal
583+
return adc_inplace(
584+
self.p_signal.copy(),
585+
self.adc_gain,
586+
self.baseline,
587+
d_nans,
588+
)
597589

598590
def dac(self, expanded=False, return_res=64, inplace=False):
599591
"""

0 commit comments

Comments
 (0)