Skip to content

Commit efca603

Browse files
authored
Merge pull request MIT-LCP#331 from MIT-LCP/multisegment-multifrequency
Read multi-frequency multi-segment records
2 parents defb4e6 + 4f06a06 commit efca603

File tree

9 files changed

+144
-30
lines changed

9 files changed

+144
-30
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
041s/2 7 125 2000 8:26:04 26/10/1994
2+
041s01 1000
3+
041s02 1000
23.4 KB
Binary file not shown.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
041s01 7 125 1000 8:26:04 26/10/1994
2+
041s01.dat 212x4 2000 12 0 168 -2716 0 III
3+
041s01.dat 212x4 2000 12 0 2 -25019 0 I
4+
041s01.dat 212x4 2000 12 0 155 -12467 0 V
5+
041s01.dat 212 20(-1600)/mmHg 12 0 -242 -18875 0 ABP
6+
041s01.dat 212 80(-1600)/mmHg 12 0 706 -5338 0 PAP
7+
041s01.dat 212 2000 12 0 -841 30145 0 PLETH
8+
041s01.dat 212 2000 12 0 401 3712 0 RESP
9+
#Produced by xform from record mimicdb/041/04100001, beginning at s74000
23.4 KB
Binary file not shown.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
041s02 7 125 1000 8:26:12 26/10/1994
2+
041s02.dat 212x4 2000 12 0 -103 -862 0 III
3+
041s02.dat 212x4 2000 12 0 -64 14967 0 I
4+
041s02.dat 212x4 2000 12 0 89 13162 0 V
5+
041s02.dat 212 20(-1600)/mmHg 12 0 -715 -21117 0 ABP
6+
041s02.dat 212 80(-1600)/mmHg 12 0 -583 -31770 0 PAP
7+
041s02.dat 212 2000 12 0 -840 -31041 0 PLETH
8+
041s02.dat 212 2000 12 0 -861 -31272 0 RESP
9+
#Produced by xform from record mimicdb/041/04100002, beginning at 0:0
26.3 KB
Binary file not shown.

tests/test_record.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,43 @@ def test_multi_fixed_c(self):
746746
np.testing.assert_equal(sig_round, sig_target)
747747
assert record.__eq__(record_named)
748748

749+
def test_multi_fixed_d(self):
750+
"""
751+
Multi-segment, fixed layout, multi-frequency, selected channels
752+
753+
Target file created with:
754+
rdsamp -r sample-data/multi-segment/041s/ -s 3 2 1 -H |
755+
cut -f 2- | sed s/-32768/-2048/ |
756+
gzip -9 -n > tests/target-output/record-multi-fixed-d.gz
757+
"""
758+
record = wfdb.rdrecord(
759+
"sample-data/multi-segment/041s/041s",
760+
channels=[3, 2, 1],
761+
physical=False,
762+
smooth_frames=False,
763+
)
764+
765+
# Convert expanded to uniform array (high-resolution)
766+
sig = np.zeros((record.sig_len * 4, record.n_sig), dtype=int)
767+
for i, s in enumerate(record.e_d_signal):
768+
sig[:, i] = np.repeat(s, len(sig[:, i]) // len(s))
769+
770+
sig_target = np.genfromtxt(
771+
"tests/target-output/record-multi-fixed-d.gz"
772+
)
773+
774+
record_named = wfdb.rdrecord(
775+
"sample-data/multi-segment/041s/041s",
776+
channel_names=["ABP", "V", "I"],
777+
physical=False,
778+
smooth_frames=False,
779+
)
780+
781+
# Sample values should match the output of rdsamp -H
782+
np.testing.assert_array_equal(sig, sig_target)
783+
# channel_names=[...] should give the same result as channels=[...]
784+
self.assertEqual(record, record_named)
785+
749786
def test_multi_variable_a(self):
750787
"""
751788
Multi-segment, variable layout, selected duration, samples read
@@ -788,7 +825,7 @@ def test_multi_variable_b(self):
788825

789826
def test_multi_variable_c(self):
790827
"""
791-
Multi-segment, variable layout, entire signal, physical
828+
Multi-segment, variable layout, entire signal, physical, expanded
792829
793830
The reference signal creation cannot be made with rdsamp
794831
directly because the WFDB c package (10.5.24) applies the single
@@ -811,9 +848,14 @@ def test_multi_variable_c(self):
811848
812849
"""
813850
record = wfdb.rdrecord(
814-
"sample-data/multi-segment/s25047/s25047-2704-05-04-10-44"
851+
"sample-data/multi-segment/s25047/s25047-2704-05-04-10-44",
852+
smooth_frames=False,
815853
)
816-
sig_round = np.round(record.p_signal, decimals=8)
854+
855+
# convert expanded to uniform array and round to 8 digits
856+
sig_round = np.zeros((record.sig_len, record.n_sig))
857+
for i in range(record.n_sig):
858+
sig_round[:, i] = np.round(record.e_p_signal[i], decimals=8)
817859

818860
sig_target_a = np.full((25740, 3), np.nan)
819861
sig_target_b = np.concatenate(

wfdb/io/_signal.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ def set_p_features(self, do_dac=False, expanded=False):
295295
self.e_p_signal = self.dac(expanded)
296296

297297
# Use e_p_signal to set fields
298-
self.check_field("e_p_signal", channels="all")
298+
self.check_field("e_p_signal", "all")
299299
self.sig_len = int(
300300
len(self.e_p_signal[0]) / self.samps_per_frame[0]
301301
)
@@ -361,7 +361,7 @@ def set_d_features(self, do_adc=False, single_fmt=True, expanded=False):
361361
if expanded:
362362
# adc is performed.
363363
if do_adc:
364-
self.check_field("e_p_signal", channels="all")
364+
self.check_field("e_p_signal", "all")
365365

366366
# If there is no fmt set it, adc_gain, and baseline
367367
if self.fmt is None:
@@ -393,7 +393,7 @@ def set_d_features(self, do_adc=False, single_fmt=True, expanded=False):
393393
self.d_signal = self.adc(expanded)
394394

395395
# Use e_d_signal to set fields
396-
self.check_field("e_d_signal", channels="all")
396+
self.check_field("e_d_signal", "all")
397397
self.sig_len = int(
398398
len(self.e_d_signal[0]) / self.samps_per_frame[0]
399399
)

wfdb/io/record.py

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -510,13 +510,6 @@ def check_read_inputs(
510510
"return_res must be one of the following when physical is True: 64, 32, 16"
511511
)
512512

513-
# Cannot expand multiple samples/frame for multi-segment records
514-
if isinstance(self, MultiRecord):
515-
if not smooth_frames:
516-
raise ValueError(
517-
"This package version cannot expand all samples when reading multi-segment records. Must enable frame smoothing."
518-
)
519-
520513
def _adjust_datetime(self, sampfrom):
521514
"""
522515
Adjust date and time fields to reflect user input if possible.
@@ -1269,7 +1262,7 @@ def _arrange_fields(
12691262
self.n_seg = len(self.segments)
12701263
self._adjust_datetime(sampfrom=sampfrom)
12711264

1272-
def multi_to_single(self, physical, return_res=64):
1265+
def multi_to_single(self, physical, return_res=64, expanded=False):
12731266
"""
12741267
Create a Record object from the MultiRecord object. All signal
12751268
segments will be combined into the new object's `p_signal` or
@@ -1283,6 +1276,11 @@ def multi_to_single(self, physical, return_res=64):
12831276
return_res : int, optional
12841277
The return resolution of the `p_signal` field. Options are:
12851278
64, 32, and 16.
1279+
expanded : bool, optional
1280+
If false, combine the sample data from `p_signal` or `d_signal`
1281+
into a single two-dimensional array. If true, combine the
1282+
sample data from `e_p_signal` or `e_d_signal` into a list of
1283+
one-dimensional arrays.
12861284
12871285
Returns
12881286
-------
@@ -1300,7 +1298,14 @@ def multi_to_single(self, physical, return_res=64):
13001298
# Figure out single segment fields to set for the new Record
13011299
if self.layout == "fixed":
13021300
# Get the fields from the first segment
1303-
for attr in ["fmt", "adc_gain", "baseline", "units", "sig_name"]:
1301+
for attr in [
1302+
"fmt",
1303+
"adc_gain",
1304+
"baseline",
1305+
"units",
1306+
"sig_name",
1307+
"samps_per_frame",
1308+
]:
13041309
fields[attr] = getattr(self.segments[0], attr)
13051310
else:
13061311
# For variable layout records, inspect the segments for the
@@ -1311,9 +1316,14 @@ def multi_to_single(self, physical, return_res=64):
13111316
# must have the same fmt, gain, baseline, and units for all
13121317
# segments.
13131318

1319+
# For either physical or digital conversion, all signals
1320+
# of the same name must have the same samps_per_frame,
1321+
# which must match the value in the layout header.
1322+
13141323
# The layout header should be updated at this point to
1315-
# reflect channels. We can depend on it for sig_name, but
1316-
# not for fmt, adc_gain, units, and baseline.
1324+
# reflect channels. We can depend on it for sig_name and
1325+
# samps_per_frame, but not for fmt, adc_gain, units, and
1326+
# baseline.
13171327

13181328
# These signal names will be the key
13191329
signal_names = self.segments[0].sig_name
@@ -1325,6 +1335,7 @@ def multi_to_single(self, physical, return_res=64):
13251335
"adc_gain": n_sig * [None],
13261336
"baseline": n_sig * [None],
13271337
"units": n_sig * [None],
1338+
"samps_per_frame": self.segments[0].samps_per_frame,
13281339
}
13291340

13301341
# For physical signals, mismatched fields will not be copied
@@ -1346,7 +1357,16 @@ def multi_to_single(self, physical, return_res=64):
13461357
reference_fields[field][ch] = item_ch
13471358
# mismatch case
13481359
elif reference_fields[field][ch] != item_ch:
1349-
if physical:
1360+
if field == "samps_per_frame":
1361+
expected = reference_fields[field][ch]
1362+
raise ValueError(
1363+
f"Incorrect samples per frame "
1364+
f"({item_ch} != {expected}) "
1365+
f"for signal {signal_names[ch]} "
1366+
f"in segment {seg.record_name} "
1367+
f"of {self.record_name}"
1368+
)
1369+
elif physical:
13501370
mismatched_fields.append(field)
13511371
else:
13521372
raise Exception(
@@ -1361,18 +1381,31 @@ def multi_to_single(self, physical, return_res=64):
13611381

13621382
# Figure out signal attribute to set, and its dtype.
13631383
if physical:
1364-
sig_attr = "p_signal"
1384+
if expanded:
1385+
sig_attr = "e_p_signal"
1386+
else:
1387+
sig_attr = "p_signal"
13651388
# Figure out the largest required dtype
13661389
dtype = _signal._np_dtype(return_res, discrete=False)
13671390
nan_vals = np.array([self.n_sig * [np.nan]], dtype=dtype)
13681391
else:
1369-
sig_attr = "d_signal"
1392+
if expanded:
1393+
sig_attr = "e_d_signal"
1394+
else:
1395+
sig_attr = "d_signal"
13701396
# Figure out the largest required dtype
13711397
dtype = _signal._np_dtype(return_res, discrete=True)
13721398
nan_vals = np.array([_signal._digi_nan(fields["fmt"])], dtype=dtype)
13731399

1400+
samps_per_frame = fields["samps_per_frame"]
1401+
13741402
# Initialize the full signal array
1375-
combined_signal = np.repeat(nan_vals, self.sig_len, axis=0)
1403+
if expanded:
1404+
combined_signal = []
1405+
for nan_val, spf in zip(nan_vals[0], samps_per_frame):
1406+
combined_signal.append(np.repeat(nan_val, spf * self.sig_len))
1407+
else:
1408+
combined_signal = np.repeat(nan_vals, self.sig_len, axis=0)
13761409

13771410
# Start and end samples in the overall array to place the
13781411
# segment samples into
@@ -1383,9 +1416,16 @@ def multi_to_single(self, physical, return_res=64):
13831416
# Copy over the signals directly. Recall there are no
13841417
# empty segments in fixed layout records.
13851418
for i in range(self.n_seg):
1386-
combined_signal[start_samps[i] : end_samps[i], :] = getattr(
1387-
self.segments[i], sig_attr
1388-
)
1419+
signals = getattr(self.segments[i], sig_attr)
1420+
if expanded:
1421+
for ch in range(self.n_sig):
1422+
start = start_samps[i] * samps_per_frame[ch]
1423+
end = end_samps[i] * samps_per_frame[ch]
1424+
combined_signal[ch][start:end] = signals[ch]
1425+
else:
1426+
start = start_samps[i]
1427+
end = end_samps[i]
1428+
combined_signal[start:end, :] = signals
13891429
else:
13901430
# Copy over the signals into the matching channels
13911431
for i in range(1, self.n_seg):
@@ -1396,12 +1436,20 @@ def multi_to_single(self, physical, return_res=64):
13961436
segment_channels = _get_wanted_channels(
13971437
fields["sig_name"], seg.sig_name, pad=True
13981438
)
1439+
signals = getattr(seg, sig_attr)
13991440
for ch in range(self.n_sig):
14001441
# Copy over relevant signal
14011442
if segment_channels[ch] is not None:
1402-
combined_signal[
1403-
start_samps[i] : end_samps[i], ch
1404-
] = getattr(seg, sig_attr)[:, segment_channels[ch]]
1443+
if expanded:
1444+
signal = signals[segment_channels[ch]]
1445+
start = start_samps[i] * samps_per_frame[ch]
1446+
end = end_samps[i] * samps_per_frame[ch]
1447+
combined_signal[ch][start:end] = signal
1448+
else:
1449+
signal = signals[:, segment_channels[ch]]
1450+
start = start_samps[i]
1451+
end = end_samps[i]
1452+
combined_signal[start:end, ch] = signal
14051453

14061454
# Create the single segment Record object and set attributes
14071455
record = Record()
@@ -1411,9 +1459,9 @@ def multi_to_single(self, physical, return_res=64):
14111459

14121460
# Use the signal to set record features
14131461
if physical:
1414-
record.set_p_features()
1462+
record.set_p_features(expanded=expanded)
14151463
else:
1416-
record.set_d_features()
1464+
record.set_d_features(expanded=expanded)
14171465

14181466
return record
14191467

@@ -4168,6 +4216,7 @@ def rdrecord(
41684216
channels=seg_channels[i],
41694217
physical=physical,
41704218
pn_dir=pn_dir,
4219+
smooth_frames=smooth_frames,
41714220
return_res=return_res,
41724221
)
41734222

@@ -4184,7 +4233,9 @@ def rdrecord(
41844233
# Convert object into a single segment Record object
41854234
if m2s:
41864235
record = record.multi_to_single(
4187-
physical=physical, return_res=return_res
4236+
physical=physical,
4237+
expanded=(not smooth_frames),
4238+
return_res=return_res,
41884239
)
41894240

41904241
# Perform dtype conversion if necessary

0 commit comments

Comments
 (0)