Skip to content

Commit 8fc10ab

Browse files
authored
Merge pull request MIT-LCP#327 from MIT-LCP/more-signal-fmts
Fix reading formats 8, 310, and 311
2 parents 34ea7a2 + 14048dd commit 8fc10ab

16 files changed

+157
-106
lines changed

.gitattributes

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
*.py diff=python
2+
3+
*.anI binary
4+
*.atr binary
5+
*.d[0-9] binary
6+
*.dat binary
7+
*.edf binary
8+
*.gz binary
9+
*.mat binary
10+
*.qrs binary
11+
*.wabp binary
12+
*.wav binary
13+
*.wqrs binary
14+
*.xyz binary

sample-data/binformats.d0

499 Bytes
Binary file not shown.

sample-data/binformats.d1

998 Bytes
Binary file not shown.

sample-data/binformats.d2

998 Bytes
Binary file not shown.

sample-data/binformats.d3

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
 .<JXft���������+9GUcq��������� (6DR`n|��������� %3AO]ky���������"0>LZhv���������-;IWes����������*8FTbp~��������� '5CQ_m{���������$2@N\jx���������!/=KYgu���������,:HVdr����������)7ESao}���������
2+
&4BP^lz���������#1?M[iw��������� .<JXft���������+9GUcq��������� (6DR`n|��������� %3AO]ky���������"0>LZhv���������-;IWes����������*8FTbp~��������� '5CQ_m{���������$2@N\jx���������!/=KYgu���������,:HVdr����������)7ESao}���������
3+
&4BP^lz���������#1?M[

sample-data/binformats.d4

998 Bytes
Binary file not shown.

sample-data/binformats.d5

749 Bytes
Binary file not shown.

sample-data/binformats.d6

666 Bytes
Binary file not shown.

sample-data/binformats.d7

666 Bytes
Binary file not shown.

sample-data/binformats.d8

1.46 KB
Binary file not shown.

sample-data/binformats.d9

1.95 KB
Binary file not shown.

sample-data/binformats.hea

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
binformats 10 200 499
2+
binformats.d0 8 200/mV 12 0 -2047 -31143 0 sig 0, fmt 8
3+
binformats.d1 16 200/mV 16 0 -32766 -750 0 sig 1, fmt 16
4+
binformats.d2 61 200/mV 16 0 -32765 -251 0 sig 2, fmt 61
5+
binformats.d3 80 200/mV 8 0 -124 -517 0 sig 3, fmt 80
6+
binformats.d4 160 200/mV 16 0 -32763 747 0 sig 4, fmt 160
7+
binformats.d5 212 200/mV 12 0 -2042 -6824 0 sig 5, fmt 212
8+
binformats.d6 310 200/mV 10 0 -505 -1621 0 sig 6, fmt 310
9+
binformats.d7 311 200/mV 10 0 -504 -2145 0 sig 7, fmt 311
10+
binformats.d8 24 200/mV 24 0 -8388599 11715 0 sig 8, fmt 24
11+
binformats.d9 32 200/mV 32 0 -2147483638 19035 0 sig 9, fmt 32

tests/target-output/record-1f.gz

15.5 KB
Binary file not shown.

tests/test_record.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,35 @@ def test_1e(self):
146146
assert record.__eq__(record_pn)
147147
assert record_2.__eq__(record_write)
148148

149+
def test_1f(self):
150+
"""
151+
All binary formats, multiple signal files in one record.
152+
153+
Target file created with:
154+
rdsamp -r sample-data/binformats | cut -f 2- |
155+
gzip -9 -n > record-1f.gz
156+
"""
157+
record = wfdb.rdrecord('sample-data/binformats', physical=False)
158+
sig_target = np.genfromtxt('tests/target-output/record-1f.gz')
159+
160+
for n, name in enumerate(record.sig_name):
161+
np.testing.assert_array_equal(
162+
record.d_signal[:, n],
163+
sig_target[:, n],
164+
"Mismatch in %s" % name)
165+
166+
for sampfrom in range(0, 3):
167+
for sampto in range(record.sig_len - 3, record.sig_len):
168+
record_2 = wfdb.rdrecord('sample-data/binformats',
169+
physical=False,
170+
sampfrom=sampfrom, sampto=sampto)
171+
for n, name in enumerate(record.sig_name):
172+
if record.fmt[n] != '8':
173+
np.testing.assert_array_equal(
174+
record_2.d_signal[:, n],
175+
sig_target[sampfrom:sampto, n],
176+
"Mismatch in %s" % name)
177+
149178
# ------------------ 2. Special format records ------------------ #
150179

151180
def test_2a(self):

wfdb/io/_signal.py

Lines changed: 72 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,7 @@ def smooth_frames(self, sigtype='physical'):
865865

866866

867867
def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset,
868-
samps_per_frame, skew, sampfrom, sampto, channels,
868+
samps_per_frame, skew, init_value, sampfrom, sampto, channels,
869869
smooth_frames, ignore_skew, no_file=False, sig_data=None, return_res=64):
870870
"""
871871
Read the digital samples from a single segment record's associated
@@ -893,6 +893,8 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset,
893893
The samples/frame for each signal of the dat file.
894894
skew : list
895895
The skew for the signals of the dat file.
896+
init_value : list
897+
The initial value for each signal of the dat file.
896898
sampfrom : int
897899
The starting sample number to be read from the signals.
898900
sampto : int
@@ -939,6 +941,7 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset,
939941
byte_offset = byte_offset[:]
940942
samps_per_frame = samps_per_frame[:]
941943
skew = skew[:]
944+
init_value = init_value[:]
942945

943946
# Set defaults for empty fields
944947
for i in range(n_sig):
@@ -948,6 +951,8 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset,
948951
samps_per_frame[i] = 1
949952
if skew[i] == None:
950953
skew[i] = 0
954+
if init_value[i] == None:
955+
init_value[i] = 0
951956

952957
# If skew is to be ignored, set all to 0
953958
if ignore_skew:
@@ -964,6 +969,7 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset,
964969
w_byte_offset = {} # one scalar per dat file
965970
w_samps_per_frame = {} # one list per dat file
966971
w_skew = {} # one list per dat file
972+
w_init_value = {} # one list per dat file
967973
w_channel = {} # one list per dat file
968974

969975
for fn in file_name:
@@ -977,6 +983,7 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset,
977983
w_byte_offset[fn] = byte_offset[datchannel[fn][0]]
978984
w_samps_per_frame[fn] = [samps_per_frame[c] for c in datchannel[fn]]
979985
w_skew[fn] = [skew[c] for c in datchannel[fn]]
986+
w_init_value[fn] = [init_value[c] for c in datchannel[fn]]
980987
w_channel[fn] = idc
981988

982989
# Wanted dat channels, relative to the dat file itself
@@ -997,17 +1004,23 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset,
9971004

9981005
# Read each wanted dat file and store signals
9991006
for fn in w_file_name:
1000-
if no_file:
1001-
signals[:, out_dat_channel[fn]] = _rd_dat_signals(fn, dir_name,
1002-
pn_dir, w_fmt[fn], len(datchannel[fn]), sig_len,
1003-
w_byte_offset[fn], w_samps_per_frame[fn], w_skew[fn],
1004-
sampfrom, sampto, smooth_frames, no_file=True,
1005-
sig_data=sig_data)[:, r_w_channel[fn]]
1006-
else:
1007-
signals[:, out_dat_channel[fn]] = _rd_dat_signals(fn, dir_name,
1008-
pn_dir, w_fmt[fn], len(datchannel[fn]), sig_len,
1009-
w_byte_offset[fn], w_samps_per_frame[fn], w_skew[fn],
1010-
sampfrom, sampto, smooth_frames)[:, r_w_channel[fn]]
1007+
datsignals = _rd_dat_signals(
1008+
file_name=fn,
1009+
dir_name=dir_name,
1010+
pn_dir=pn_dir,
1011+
fmt=w_fmt[fn],
1012+
n_sig=len(datchannel[fn]),
1013+
sig_len=sig_len,
1014+
byte_offset=w_byte_offset[fn],
1015+
samps_per_frame=w_samps_per_frame[fn],
1016+
skew=w_skew[fn],
1017+
init_value=w_init_value[fn],
1018+
sampfrom=sampfrom,
1019+
sampto=sampto,
1020+
smooth_frames=smooth_frames,
1021+
no_file=no_file,
1022+
sig_data=sig_data)
1023+
signals[:, out_dat_channel[fn]] = datsignals[:, r_w_channel[fn]]
10111024

10121025
# Return each sample in signals with multiple samples/frame, without smoothing.
10131026
# Return a list of numpy arrays for each signal.
@@ -1016,16 +1029,22 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset,
10161029

10171030
for fn in w_file_name:
10181031
# Get the list of all signals contained in the dat file
1019-
if no_file:
1020-
datsignals = _rd_dat_signals(fn, dir_name, pn_dir, w_fmt[fn],
1021-
len(datchannel[fn]), sig_len, w_byte_offset[fn],
1022-
w_samps_per_frame[fn], w_skew[fn], sampfrom, sampto,
1023-
smooth_frames, no_file=True, sig_data=sig_data)
1024-
else:
1025-
datsignals = _rd_dat_signals(fn, dir_name, pn_dir, w_fmt[fn],
1026-
len(datchannel[fn]), sig_len, w_byte_offset[fn],
1027-
w_samps_per_frame[fn], w_skew[fn], sampfrom, sampto,
1028-
smooth_frames)
1032+
datsignals = _rd_dat_signals(
1033+
file_name=fn,
1034+
dir_name=dir_name,
1035+
pn_dir=pn_dir,
1036+
fmt=w_fmt[fn],
1037+
n_sig=len(datchannel[fn]),
1038+
sig_len=sig_len,
1039+
byte_offset=w_byte_offset[fn],
1040+
samps_per_frame=w_samps_per_frame[fn],
1041+
skew=w_skew[fn],
1042+
init_value=w_init_value[fn],
1043+
sampfrom=sampfrom,
1044+
sampto=sampto,
1045+
smooth_frames=smooth_frames,
1046+
no_file=no_file,
1047+
sig_data=sig_data)
10291048

10301049
# Copy over the wanted signals
10311050
for cn in range(len(out_dat_channel[fn])):
@@ -1035,8 +1054,9 @@ def _rd_segment(file_name, dir_name, pn_dir, fmt, n_sig, sig_len, byte_offset,
10351054

10361055

10371056
def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len,
1038-
byte_offset, samps_per_frame, skew, sampfrom, sampto,
1039-
smooth_frames, no_file=False, sig_data=None):
1057+
byte_offset, samps_per_frame, skew, init_value,
1058+
sampfrom, sampto, smooth_frames,
1059+
no_file=False, sig_data=None):
10401060
"""
10411061
Read all signals from a WFDB dat file.
10421062
@@ -1062,6 +1082,8 @@ def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len,
10621082
The samples/frame for each signal of the dat file.
10631083
skew : list
10641084
The skew for the signals of the dat file.
1085+
init_value : list
1086+
The initial value for each signal of the dat file.
10651087
sampfrom : int
10661088
The starting sample number to be read from the signals.
10671089
sampto : int
@@ -1160,6 +1182,32 @@ def _rd_dat_signals(file_name, dir_name, pn_dir, fmt, n_sig, sig_len,
11601182
elif fmt == '160':
11611183
sig_data = (sig_data.astype('int32') - 32768).astype('int16')
11621184

1185+
# For format 8, convert sample differences to absolute samples. Note
1186+
# that if sampfrom is not 0, the results will be wrong, since we can't
1187+
# know the starting value without reading the entire record from the
1188+
# beginning - an inherent limitation of the format, and the use of
1189+
# format 8 is discouraged for this reason! However, the following is
1190+
# consistent with the behavior of the WFDB library: the initial value
1191+
# specified by the header file is used as the starting sample value,
1192+
# regardless of where in the record we begin reading. Therefore, the
1193+
# following should give the same results as rdsamp.
1194+
if fmt == '8':
1195+
dif_frames = sig_data.reshape(-1, tsamps_per_frame)
1196+
abs_frames = np.empty(dif_frames.shape, dtype='int32')
1197+
ch_start = 0
1198+
for ch in range(n_sig):
1199+
ch_end = ch_start + samps_per_frame[ch]
1200+
# Extract sample differences as a 2D array
1201+
ch_dif_signal = dif_frames[:, ch_start:ch_end]
1202+
# Convert to a 1D array of absolute samples
1203+
ch_abs_signal = ch_dif_signal.cumsum(dtype=abs_frames.dtype)
1204+
ch_abs_signal += init_value[ch]
1205+
# Transfer to the output array
1206+
ch_abs_signal = ch_abs_signal.reshape(ch_dif_signal.shape)
1207+
abs_frames[:, ch_start:ch_end] = ch_abs_signal
1208+
ch_start = ch_end
1209+
sig_data = abs_frames.reshape(-1)
1210+
11631211
# At this point, dtype of sig_data is the minimum integer format
11641212
# required for storing the final digital samples.
11651213

@@ -1472,14 +1520,6 @@ def _blocks_to_samples(sig_data, n_samp, fmt):
14721520
sig[sig > 2047] -= 4096
14731521

14741522
elif fmt == '310':
1475-
# Easier to process when dealing with whole blocks
1476-
if n_samp % 3:
1477-
n_samp = upround(n_samp,3)
1478-
added_samps = n_samp % 3
1479-
sig_data = np.append(sig_data, np.zeros(added_samps, dtype='uint8'))
1480-
else:
1481-
added_samps = 0
1482-
14831523
sig_data = sig_data.astype('int16')
14841524
sig = np.zeros(n_samp, dtype='int16')
14851525

@@ -1491,24 +1531,11 @@ def _blocks_to_samples(sig_data, n_samp, fmt):
14911531
# Third signal is 5 msb of second byte and 5 msb of forth byte
14921532
sig[2::3] = np.bitwise_and((sig_data[1::4] >> 3), 0x1f)[0:len(sig[2::3])] + 32 * np.bitwise_and(sig_data[3::4] >> 3, 0x1f)[0:len(sig[2::3])]
14931533

1494-
# Remove trailing samples read within the byte block if
1495-
# originally not 3n sampled
1496-
if added_samps:
1497-
sig = sig[:-added_samps]
1498-
14991534
# Loaded values as un_signed. Convert to 2's complement form:
15001535
# values > 2^9-1 are negative.
15011536
sig[sig > 511] -= 1024
15021537

15031538
elif fmt == '311':
1504-
# Easier to process when dealing with whole blocks
1505-
if n_samp % 3:
1506-
n_samp = upround(n_samp,3)
1507-
added_samps = n_samp % 3
1508-
sig_data = np.append(sig_data, np.zeros(added_samps, dtype='uint8'))
1509-
else:
1510-
added_samps = 0
1511-
15121539
sig_data = sig_data.astype('int16')
15131540
sig = np.zeros(n_samp, dtype='int16')
15141541

@@ -1520,11 +1547,6 @@ def _blocks_to_samples(sig_data, n_samp, fmt):
15201547
# Third sample is 4 msb of third byte and 6 msb of forth byte
15211548
sig[2::3] = (sig_data[2::4] >> 4)[0:len(sig[2::3])] + 16 * np.bitwise_and(sig_data[3::4], 0x7f)[0:len(sig[2::3])]
15221549

1523-
# Remove trailing samples read within the byte block if
1524-
# originally not 3n sampled
1525-
if added_samps:
1526-
sig = sig[:-added_samps]
1527-
15281550
# Loaded values as un_signed. Convert to 2's complement form.
15291551
# Values > 2^9-1 are negative.
15301552
sig[sig > 511] -= 1024

wfdb/io/record.py

Lines changed: 28 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3507,39 +3507,38 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None,
35073507

35083508
# A single segment record
35093509
elif isinstance(record, Record):
3510+
if record_name.endswith('.edf') or record_name.endswith('.wav'):
3511+
no_file = True
3512+
sig_data = record.d_signal
3513+
else:
3514+
no_file = False
3515+
sig_data = None
3516+
3517+
signals = _signal._rd_segment(
3518+
file_name=record.file_name,
3519+
dir_name=dir_name,
3520+
pn_dir=pn_dir,
3521+
fmt=record.fmt,
3522+
n_sig=record.n_sig,
3523+
sig_len=record.sig_len,
3524+
byte_offset=record.byte_offset,
3525+
samps_per_frame=record.samps_per_frame,
3526+
skew=record.skew,
3527+
init_value=record.init_value,
3528+
sampfrom=sampfrom,
3529+
sampto=sampto,
3530+
channels=channels,
3531+
smooth_frames=smooth_frames,
3532+
ignore_skew=ignore_skew,
3533+
no_file=no_file,
3534+
sig_data=sig_data,
3535+
return_res=return_res)
35103536

35113537
# Only 1 sample/frame, or frames are smoothed. Return uniform numpy array
35123538
if smooth_frames or max([record.samps_per_frame[c] for c in channels]) == 1:
35133539
# Read signals from the associated dat files that contain
35143540
# wanted channels
3515-
if record_name.endswith('.edf') or record_name.endswith('.wav'):
3516-
record.d_signal = _signal._rd_segment(record.file_name,
3517-
dir_name, pn_dir,
3518-
record.fmt,
3519-
record.n_sig,
3520-
record.sig_len,
3521-
record.byte_offset,
3522-
record.samps_per_frame,
3523-
record.skew, sampfrom,
3524-
sampto, channels,
3525-
smooth_frames,
3526-
ignore_skew,
3527-
no_file=True,
3528-
sig_data=record.d_signal,
3529-
return_res=return_res)
3530-
else:
3531-
record.d_signal = _signal._rd_segment(record.file_name,
3532-
dir_name, pn_dir,
3533-
record.fmt,
3534-
record.n_sig,
3535-
record.sig_len,
3536-
record.byte_offset,
3537-
record.samps_per_frame,
3538-
record.skew, sampfrom,
3539-
sampto, channels,
3540-
smooth_frames,
3541-
ignore_skew,
3542-
return_res=return_res)
3541+
record.d_signal = signals
35433542

35443543
# Arrange/edit the object fields to reflect user channel
35453544
# and/or signal range input
@@ -3552,34 +3551,7 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None,
35523551

35533552
# Return each sample of the signals with multiple samples per frame
35543553
else:
3555-
if record_name.endswith('.edf') or record_name.endswith('.wav'):
3556-
record.e_d_signal = _signal._rd_segment(record.file_name,
3557-
dir_name, pn_dir,
3558-
record.fmt,
3559-
record.n_sig,
3560-
record.sig_len,
3561-
record.byte_offset,
3562-
record.samps_per_frame,
3563-
record.skew, sampfrom,
3564-
sampto, channels,
3565-
smooth_frames,
3566-
ignore_skew,
3567-
no_file=True,
3568-
sig_data=record.d_signal,
3569-
return_res=return_res)
3570-
else:
3571-
record.e_d_signal = _signal._rd_segment(record.file_name,
3572-
dir_name, pn_dir,
3573-
record.fmt,
3574-
record.n_sig,
3575-
record.sig_len,
3576-
record.byte_offset,
3577-
record.samps_per_frame,
3578-
record.skew, sampfrom,
3579-
sampto, channels,
3580-
smooth_frames,
3581-
ignore_skew,
3582-
return_res=return_res)
3554+
record.e_d_signal = signals
35833555

35843556
# Arrange/edit the object fields to reflect user channel
35853557
# and/or signal range input

0 commit comments

Comments
 (0)