Skip to content

Commit 1831d87

Browse files
committed
update signal line regex to capture ~ record names in layout headers
1 parent ffbf556 commit 1831d87

23 files changed

+146
-102
lines changed
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

tests/test_io.py renamed to tests/test_record.py

Lines changed: 70 additions & 43 deletions
Large diffs are not rendered by default.

wfdb/io/_header.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -115,28 +115,26 @@
115115

116116
# Regexp objects for reading headers
117117

118-
# Record Line Fields
119-
_rx_record = re.compile(
120-
''.join(
121-
[
122-
"(?P<record_name>[-\w]+)/?(?P<n_seg>\d*)[ \t]+",
123-
"(?P<n_sig>\d+)[ \t]*",
124-
"(?P<fs>\d*\.?\d*)/*(?P<counterfs>\d*\.?\d*)\(?(?P<base_counter>\d*\.?\d*)\)?[ \t]*",
125-
"(?P<sig_len>\d*)[ \t]*",
126-
"(?P<base_time>\d{,2}:?\d{,2}:?\d{,2}\.?\d{,6})[ \t]*",
127-
"(?P<base_date>\d{,2}/?\d{,2}/?\d{,4})"]))
128-
129-
# Signal Line Fields
130-
_rx_signal = re.compile(
131-
''.join(
132-
[
133-
"(?P<file_name>[-\w]+\.?[\w]*~?)[ \t]+(?P<fmt>\d+)x?"
134-
"(?P<samps_per_frame>\d*):?(?P<skew>\d*)\+?(?P<byte_offset>\d*)[ \t]*",
135-
"(?P<adc_gain>-?\d*\.?\d*e?[\+-]?\d*)\(?(?P<baseline>-?\d*)\)?/?(?P<units>[\w\^\-\?%]*)[ \t]*",
136-
"(?P<adc_res>\d*)[ \t]*(?P<adc_zero>-?\d*)[ \t]*(?P<init_value>-?\d*)[ \t]*",
137-
"(?P<checksum>-?\d*)[ \t]*(?P<block_size>\d*)[ \t]*(?P<sig_name>[\S]?[^\t\n\r\f\v]*)"]))
138-
139-
# Segment Line Fields
118+
# Record line
119+
_rx_record = re.compile(''.join(
120+
["(?P<record_name>[-\w]+)/?(?P<n_seg>\d*)[ \t]+",
121+
"(?P<n_sig>\d+)[ \t]*",
122+
"(?P<fs>\d*\.?\d*)/*(?P<counterfs>\d*\.?\d*)\(?(?P<base_counter>\d*\.?\d*)\)?[ \t]*",
123+
"(?P<sig_len>\d*)[ \t]*",
124+
"(?P<base_time>\d{,2}:?\d{,2}:?\d{,2}\.?\d{,6})[ \t]*",
125+
"(?P<base_date>\d{,2}/?\d{,2}/?\d{,4})"])
126+
)
127+
128+
# Signal line
129+
_rx_signal = re.compile(''.join(
130+
["(?P<file_name>~?[-\w]*\.?[\w]*)[ \t]+(?P<fmt>\d+)x?"
131+
"(?P<samps_per_frame>\d*):?(?P<skew>\d*)\+?(?P<byte_offset>\d*)[ \t]*",
132+
"(?P<adc_gain>-?\d*\.?\d*e?[\+-]?\d*)\(?(?P<baseline>-?\d*)\)?/?(?P<units>[\w\^\-\?%]*)[ \t]*",
133+
"(?P<adc_res>\d*)[ \t]*(?P<adc_zero>-?\d*)[ \t]*(?P<init_value>-?\d*)[ \t]*",
134+
"(?P<checksum>-?\d*)[ \t]*(?P<block_size>\d*)[ \t]*(?P<sig_name>[\S]?[^\t\n\r\f\v]*)"])
135+
)
136+
137+
# Segment line
140138
_rx_segment = re.compile('(?P<seg_name>\w*~?)[ \t]+(?P<seg_len>\d+)')
141139

142140

@@ -254,6 +252,8 @@ def set_defaults(self):
254252
- This is not responsible for initializing the attributes. That
255253
is done by the constructor.
256254
255+
See also `set_p_features` and `set_d_features`.
256+
257257
"""
258258
rfields, sfields = self.get_write_fields()
259259
for f in rfields:

wfdb/io/record.py

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
# set_p_features and set_d_features use characteristics of the p_signal or d_signal field to fill in other header fields.
2-
# These are separate from another method 'set_defaults' which the user may call to set default header fields
3-
41
import datetime
52
import multiprocessing
63
import posixpath
@@ -45,8 +42,9 @@ def check_field(self, field, required_channels='all'):
4542
field : str
4643
The field name
4744
required_channels : list, optional
48-
Used for signal specification fields. Species the channels
49-
to check. Other channels can be None.
45+
Used for signal specification fields. All channels are
46+
checked for their integrity if present, but channels that do
47+
not lie in this field may be None.
5048
5149
Notes
5250
-----
@@ -233,6 +231,30 @@ def check_read_inputs(self, sampfrom, sampto, channels, physical,
233231
if smooth_frames is False:
234232
raise ValueError('This package version cannot expand all samples when reading multi-segment records. Must enable frame smoothing.')
235233

234+
def _adjust_datetime(self, sampfrom):
235+
"""
236+
Adjust date and time fields to reflect user input if possible.
237+
238+
Helper function for the `_arrange_fields` of both Record and
239+
MultiRecord objects.
240+
"""
241+
if sampfrom:
242+
dt_seconds = sampfrom / self.fs
243+
if self.base_date and self.base_time:
244+
self.base_datetime = datetime.datetime.combine(self.base_date,
245+
self.base_time)
246+
self.base_datetime += datetime.timedelta(seconds=dt_seconds)
247+
self.base_date = self.base_datetime.date()
248+
self.base_time = self.base_datetime.time()
249+
# We can calculate the time even if there is no date
250+
elif self.base_time:
251+
tmp_datetime = datetime.datetime.combine(
252+
datetime.datetime.today().date(), self.base_time)
253+
self.base_time = (tmp_datetime
254+
+ datetime.timedelta(seconds=dt_seconds)).time()
255+
# Cannot calculate date or time if there is only date
256+
257+
236258

237259
class Record(BaseRecord, _header.HeaderMixin, _signal.SignalMixin):
238260
"""
@@ -389,22 +411,8 @@ def _arrange_fields(self, channels, sampfrom=0, expanded=False):
389411
self.n_sig = len(channels)
390412
self.sig_len = self.d_signal.shape[0]
391413

392-
# Set and adjust time and date if possible
393-
if sampfrom:
394-
dt_seconds = sampfrom / self.fs
395-
if self.base_date and self.base_time:
396-
self.base_datetime = datetime.datetime.combine(self.base_date,
397-
self.base_time)
398-
self.base_datetime += datetime.timedelta(seconds=dt_seconds)
399-
self.base_date = self.base_datetime.date()
400-
self.base_time = self.base_datetime.time()
401-
# We can calculate the time even if there is no date
402-
elif self.base_time:
403-
tmp_datetime = datetime.datetime.combine(
404-
datetime.datetime.today().date(), self.base_time)
405-
self.base_time = (tmp_datetime
406-
+ datetime.timedelta(seconds=dt_seconds)).time()
407-
# Cannot calculate date or time if there is only date
414+
# Adjust date and time if necessary
415+
self._adjust_datetime(sampfrom=sampfrom)
408416

409417

410418
class MultiRecord(BaseRecord, _header.MultiHeaderMixin):
@@ -473,24 +481,25 @@ def wrsamp(self, write_dir=''):
473481
for seg in self.segments:
474482
seg.wrsamp(write_dir=write_dir)
475483

484+
def _check_segment_cohesion(self):
485+
"""
486+
Check the cohesion of the segments field with other fields used
487+
to write the record
488+
"""
476489

477-
# Check the cohesion of the segments field with other fields used to write the record
478-
def checksegmentcohesion(self):
479-
480-
# Check that n_seg is equal to the length of the segments field
481490
if self.n_seg != len(self.segments):
482491
raise ValueError("Length of segments must match the 'n_seg' field")
483492

484-
for i in range(0, n_seg):
493+
for i in range(n_seg):
485494
s = self.segments[i]
486495

487496
# If segment 0 is a layout specification record, check that its file names are all == '~''
488-
if i==0 and self.seg_len[0] == 0:
497+
if i == 0 and self.seg_len[0] == 0:
489498
for file_name in s.file_name:
490499
if file_name != '~':
491500
raise ValueError("Layout specification records must have all file_names named '~'")
492501

493-
# Check that sampling frequencies all match the one in the master header
502+
# Sampling frequencies must all match the one in the master header
494503
if s.fs != self.fs:
495504
raise ValueError("The 'fs' in each segment must match the overall record's 'fs'")
496505

@@ -505,7 +514,7 @@ def checksegmentcohesion(self):
505514

506515

507516

508-
def _requiresegment_fieldsments(self, sampfrom, sampto, channels):
517+
def _required_segments(self, sampfrom, sampto, channels):
509518
"""
510519
Determine the segments and the samples within each segment that
511520
have to be read in a multi-segment record.
@@ -609,7 +618,7 @@ def _get_required_channels(self, seg_numbers, channels, dirname, pb_dir):
609618

610619
return required_channels
611620

612-
def _arrange_fields(self, seg_numbers, seg_ranges, channels):
621+
def _arrange_fields(self, seg_numbers, seg_ranges, channels, sampfrom=0):
613622
"""
614623
Arrange/edit object fields to reflect user channel and/or
615624
signal range inputs.
@@ -639,6 +648,8 @@ def _arrange_fields(self, seg_numbers, seg_ranges, channels):
639648
# Update number of segments
640649
self.n_seg = len(self.segments)
641650

651+
# Adjust date and time if necessary
652+
self._adjust_datetime(sampfrom=sampfrom)
642653

643654
def multi_to_single(self, physical, return_res=64):
644655
"""
@@ -690,6 +701,8 @@ def multi_to_single(self, physical, return_res=64):
690701
# pass the test.
691702
if self.layout == 'variable':
692703
for seg in self.segments[1:]:
704+
if seg is None:
705+
continue
693706
segment_channels = get_wanted_channels(fields['sig_name'],
694707
seg.sig_name,
695708
pad=True)
@@ -699,6 +712,7 @@ def multi_to_single(self, physical, return_res=64):
699712
if segment_channels[ch] is None:
700713
continue
701714
if getattr(seg, attr)[segment_channels[ch]] != fields[attr][ch]:
715+
702716
raise Exception('This variable layout multi-segment record cannot be converted to single segment, in digital format.')
703717

704718
sig_attr = 'd_signal'
@@ -1110,10 +1124,11 @@ def rdrecord(record_name, sampfrom=0, sampto='end', channels='all',
11101124
pb_dir=pb_dir)
11111125

11121126
# The segment numbers and samples within each segment to read.
1113-
seg_numbers, seg_ranges = record._requiresegment_fieldsments(sampfrom, sampto,
1114-
channels)
1127+
seg_numbers, seg_ranges = record._required_segments(sampfrom, sampto,
1128+
channels)
11151129
# The channels within each segment to read
1116-
seg_channels = record._get_required_channels(seg_numbers, channels, dirname, pb_dir)
1130+
seg_channels = record._get_required_channels(seg_numbers, channels,
1131+
dirname, pb_dir)
11171132

11181133
# Read the desired samples in the relevant segments
11191134
for i in range(len(seg_numbers)):
@@ -1128,7 +1143,8 @@ def rdrecord(record_name, sampfrom=0, sampto='end', channels='all',
11281143
channels=seg_channels[i], physical=physical, pb_dir=pb_dir)
11291144

11301145
# Arrange the fields of the overall object to reflect user input
1131-
record._arrange_fields(seg_numbers, seg_ranges, channels)
1146+
record._arrange_fields(seg_numbers=seg_numbers, seg_ranges=seg_ranges,
1147+
channels=channels, sampfrom=sampfrom)
11321148

11331149
# Convert object into a single segment Record object
11341150
if m2s:
@@ -1246,8 +1262,8 @@ def get_wanted_channels(wanted_sig_names, record_sig_names, pad=False):
12461262

12471263

12481264
def wrsamp(record_name, fs, units, sig_name, p_signal=None, d_signal=None,
1249-
fmt=None, adc_gain=None, baseline=None, comments=None, base_time=None,
1250-
base_date=None, write_dir=''):
1265+
fmt=None, adc_gain=None, baseline=None, comments=None,
1266+
base_time=None, base_date=None, write_dir=''):
12511267
"""
12521268
Write a single segment WFDB record, creating a WFDB header file and any
12531269
associated dat files.
@@ -1324,7 +1340,8 @@ def wrsamp(record_name, fs, units, sig_name, p_signal=None, d_signal=None,
13241340
if d_signal is not None:
13251341
if fmt is None or adc_gain is None or baseline is None:
13261342
raise Exception("When using d_signal, must also specify 'fmt', 'gain', and 'baseline' fields.")
1327-
# Depending on whether d_signal or p_signal was used, set other required features.
1343+
# Depending on whether d_signal or p_signal was used, set other
1344+
# required features.
13281345
if p_signal is not None:
13291346
# Create the Record object
13301347
record = Record(record_name=record_name, p_signal=p_signal, fs=fs,

0 commit comments

Comments
 (0)