Skip to content

Commit e56ce93

Browse files
committed
rdheader as module. close MIT-LCP#29
1 parent 30b7655 commit e56ce93

File tree

3 files changed

+193
-186
lines changed

3 files changed

+193
-186
lines changed

wfdb/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from ._rdsamp import rdsamp
22
from ._rdann import rdann
3+
from ._rdheader import rdheader
34
from ._plotwfdb import plotwfdb

wfdb/_rdheader.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import numpy as np
2+
import re
3+
import os
4+
import sys
5+
import requests
6+
7+
def rdheader(recordname): # For reading signal headers
8+
9+
# To do: Allow exponential input format for some fields
10+
11+
# Output dictionary
12+
fields = {
13+
'nseg': [],
14+
'nsig': [],
15+
'fs': [],
16+
'nsamp': [],
17+
'basetime': [],
18+
'basedate': [],
19+
'filename': [],
20+
'fmt': [],
21+
'sampsperframe': [],
22+
'skew': [],
23+
'byteoffset': [],
24+
'gain': [],
25+
'units': [],
26+
'baseline': [],
27+
'initvalue': [],
28+
'signame': [],
29+
'nsampseg': [],
30+
'comments': []}
31+
# filename stores file names for both multi and single segment headers.
32+
# nsampseg is only for multi-segment
33+
34+
35+
# RECORD LINE fields (o means optional, delimiter is space or tab unless specified):
36+
# record name, nsegments (o, delim=/), nsignals, fs (o), counter freq (o, delim=/, needs fs),
37+
# base counter (o, delim=(), needs counter freq), nsamples (o, needs fs), base time (o),
38+
# base date (o needs base time).
39+
40+
# Regexp object for record line
41+
rxRECORD = re.compile(
42+
''.join(
43+
[
44+
"(?P<name>[\w]+)/?(?P<nseg>\d*)[ \t]+",
45+
"(?P<nsig>\d+)[ \t]*",
46+
"(?P<fs>\d*\.?\d*)/*(?P<counterfs>\d*\.?\d*)\(?(?P<basecounter>\d*\.?\d*)\)?[ \t]*",
47+
"(?P<nsamples>\d*)[ \t]*",
48+
"(?P<basetime>\d*:?\d{,2}:?\d{,2}\.?\d*)[ \t]*",
49+
"(?P<basedate>\d{,2}/?\d{,2}/?\d{,4})"]))
50+
# Watch out for potential floats: fs (and also exponent notation...),
51+
# counterfs, basecounter
52+
53+
# SIGNAL LINE fields (o means optional, delimiter is space or tab unless specified):
54+
# file name, format, samplesperframe(o, delim=x), skew(o, delim=:), byteoffset(o,delim=+),
55+
# ADCgain(o), baseline(o, delim=(), requires ADCgain), units(o, delim=/, requires baseline),
56+
# ADCres(o, requires ADCgain), ADCzero(o, requires ADCres), initialvalue(o, requires ADCzero),
57+
# checksum(o, requires initialvalue), blocksize(o, requires checksum),
58+
# signame(o, requires block)
59+
60+
# Regexp object for signal lines. Consider flexible filenames, and also ~
61+
rxSIGNAL = re.compile(
62+
''.join(
63+
[
64+
"(?P<filename>[\w]*\.?[\w]*~?)[ \t]+(?P<format>\d+)x?"
65+
"(?P<sampsperframe>\d*):?(?P<skew>\d*)\+?(?P<byteoffset>\d*)[ \t]*",
66+
"(?P<ADCgain>-?\d*\.?\d*e?[\+-]?\d*)\(?(?P<baseline>-?\d*)\)?/?(?P<units>[\w\^/-]*)[ \t]*",
67+
"(?P<ADCres>\d*)[ \t]*(?P<ADCzero>-?\d*)[ \t]*(?P<initialvalue>-?\d*)[ \t]*",
68+
"(?P<checksum>-?\d*)[ \t]*(?P<blocksize>\d*)[ \t]*(?P<signame>[\S]*)"]))
69+
70+
# Units characters: letters, numbers, /, ^, -,
71+
# Watch out for potentially negative fields: baseline, ADCzero, initialvalue, checksum,
72+
# Watch out for potential float: ADCgain.
73+
74+
# Read the header file and get the comment and non-comment lines
75+
headerlines, commentlines = getheaderlines(recordname)
76+
77+
# Get record line parameters
78+
(_, nseg, nsig, fs, counterfs, basecounter, nsamp,
79+
basetime, basedate) = rxRECORD.findall(headerlines[0])[0]
80+
81+
# These fields are either mandatory or set to defaults.
82+
if not nseg:
83+
nseg = '1'
84+
if not fs:
85+
fs = '250'
86+
87+
fields['nseg'] = int(nseg)
88+
fields['fs'] = float(fs)
89+
fields['nsig'] = int(nsig)
90+
91+
# These fields might by empty
92+
if nsamp:
93+
fields['nsamp'] = int(nsamp)
94+
fields['basetime'] = basetime
95+
fields['basedate'] = basedate
96+
97+
98+
# Signal or Segment line paramters
99+
# Multi segment header - Process segment spec lines in current master
100+
# header.
101+
if int(nseg) > 1:
102+
for i in range(0, int(nseg)):
103+
(filename, nsampseg) = re.findall(
104+
'(?P<filename>\w*~?)[ \t]+(?P<nsampseg>\d+)', headerlines[i + 1])[0]
105+
fields["filename"].append(filename)
106+
fields["nsampseg"].append(int(nsampseg))
107+
# Single segment header - Process signal spec lines in regular header.
108+
else:
109+
for i in range(0, int(nsig)): # will not run if nsignals=0
110+
# get signal line parameters
111+
(filename,
112+
fmt,
113+
sampsperframe,
114+
skew,
115+
byteoffset,
116+
adcgain,
117+
baseline,
118+
units,
119+
adcres,
120+
adczero,
121+
initvalue,
122+
checksum,
123+
blocksize,
124+
signame) = rxSIGNAL.findall(headerlines[i + 1])[0]
125+
126+
# Setting defaults
127+
if not sampsperframe:
128+
# Setting strings here so we can always convert strings case
129+
# below.
130+
sampsperframe = '1'
131+
if not skew:
132+
skew = '0'
133+
if not byteoffset:
134+
byteoffset = '0'
135+
if not adcgain:
136+
adcgain = '200'
137+
if not baseline:
138+
if not adczero:
139+
baseline = '0'
140+
else:
141+
baseline = adczero # missing baseline actually takes adczero value if present
142+
if not units:
143+
units = 'mV'
144+
if not initvalue:
145+
initvalue = '0'
146+
if not signame:
147+
signame = "ch" + str(i + 1)
148+
if not initvalue:
149+
initvalue = '0'
150+
151+
fields["filename"].append(filename)
152+
fields["fmt"].append(fmt)
153+
fields["sampsperframe"].append(int(sampsperframe))
154+
fields["skew"].append(int(skew))
155+
fields['byteoffset'].append(int(byteoffset))
156+
fields["gain"].append(float(adcgain))
157+
fields["baseline"].append(int(baseline))
158+
fields["units"].append(units)
159+
fields["initvalue"].append(int(initvalue))
160+
fields["signame"].append(signame)
161+
162+
for comment in commentlines:
163+
fields["comments"].append(comment.strip('\s#'))
164+
165+
return fields
166+
167+
168+
# Read header file to get comment and non-comment lines
169+
def getheaderlines(recordname):
170+
with open(recordname + ".hea", 'r') as fp:
171+
headerlines = [] # Store record line followed by the signal lines if any
172+
commentlines = [] # Comments
173+
for line in fp:
174+
line = line.strip()
175+
if line.startswith('#'): # comment line
176+
commentlines.append(line)
177+
elif line: # Non-empty non-comment line = header line.
178+
ci = line.find('#')
179+
if ci > 0:
180+
headerlines.append(line[:ci]) # header line
181+
# comment on same line as header line
182+
commentlines.append(line[ci:])
183+
else:
184+
headerlines.append(line)
185+
return headerlines, commentlines
186+

0 commit comments

Comments
 (0)