Skip to content

Commit 8651db3

Browse files
committed
v0.6
1 parent e9e45d1 commit 8651db3

File tree

5 files changed

+256
-1
lines changed

5 files changed

+256
-1
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def readme():
1111
setup(
1212
name = 'signalz',
1313
packages = find_packages(exclude=("tests",)),
14-
version = '0.5',
14+
version = '0.6',
1515
description = 'Synthetic data generators in Python',
1616
long_description = readme(),
1717
author = 'Matous Cejnek',

signalz/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@
146146
:ref:`tags-population_model`, :ref:`tags-chaotic`
147147
148148
149+
.. cssclass:: funcitem
150+
151+
* :ref:`generators-ecgsyn`
152+
153+
.. cssclass:: functag
154+
155+
:ref:`tags-biosignal`
156+
149157
150158
Contact
151159
=====================

signalz/generators/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from signalz.generators.autoregressive_model import autoregressive_model
33
from signalz.generators.mackey_glass import mackey_glass
44
from signalz.generators.logistic_map import logistic_map
5+
from signalz.generators.ecgsyn import ecgsyn
56

67
# noises and walks
78
from signalz.generators.gaussian_white_noise import gaussian_white_noise
@@ -17,3 +18,4 @@
1718
from signalz.generators.steps import steps
1819
from signalz.generators.random_steps import random_steps
1920

21+

signalz/generators/ecgsyn.py

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"""
2+
.. versionadded:: 0.6
3+
4+
This functions produces a synthesized ECG signal. User can control following
5+
parameters: mean heart rate, number of beats, sampling frequency,
6+
waveform morphology (P, Q, R, S, and T timing, amplitude,and duration),
7+
standard deviation of the RR interval, LF/HF ratio.
8+
9+
Copyright (c) 2003 by Patrick McSharry & Gari Clifford, All Rights Reserved
10+
:cite:`mcsharry2003dynamical`.
11+
12+
More information can be found in PhysionNet :cite:`mcsharry2003ecgsyn`.
13+
14+
Example Usage
15+
===============
16+
17+
Simple example follows
18+
19+
.. code-block:: python
20+
21+
x, peaks = ecgsyn(n=10, hrmean=50, hrstd=3, sfecg=128)
22+
23+
The returned variable `x` contains the synthetic ECG series.
24+
25+
References
26+
============
27+
28+
.. bibliography:: ecgsyn.bib
29+
:style: plain
30+
31+
Function Documentation
32+
======================================
33+
"""
34+
import numpy as np
35+
36+
from signalz.misc import check_type_or_raise, ode45, rem
37+
38+
def derfunc(t, x, rr, sfint, ti, ai, bi):
39+
"""
40+
Derivations of the ECG function.
41+
"""
42+
xi = np.cos(ti)
43+
yi = np.sin(ti)
44+
ta = np.arctan2(x[1], x[0])
45+
r0 = 1.
46+
a0 = 1. - np.sqrt((x[0]**2) + (x[1]**2)) / r0
47+
ip = int(np.floor(t * sfint))
48+
w0 = 2. * np.pi / rr[ip]
49+
fresp = 0.25
50+
zbase = 0.005 * np.sin(2 * np.pi * fresp * t)
51+
dx1dt = a0*x[0] - w0*x[1]
52+
dx2dt = a0*x[1] + w0*x[0]
53+
dti = rem(ta - ti, 2. * np.pi)
54+
dx3dt = - np.sum(ai * dti * np.exp(-0.5*(dti / bi)**2)) - (x[2] - zbase)
55+
return np.array([dx1dt, dx2dt, dx3dt])
56+
57+
def rrprocess(flo, fhi, flostd, fhistd, lfhfratio, hrmean, hrstd, sfrr, n):
58+
w1 = 2. * np.pi * flo
59+
w2 = 2. * np.pi * fhi
60+
c1 = 2. * np.pi * flostd
61+
c2 = 2. * np.pi * fhistd
62+
sig2 = 1
63+
sig1 = lfhfratio
64+
rrmean = 60 / hrmean
65+
rrstd = 60 * hrstd / float(hrmean * hrmean)
66+
df = sfrr / float(n)
67+
w = np.arange(0,n) * 2 * np.pi * df
68+
dw1 = w - w1
69+
dw2 = w - w2
70+
Hw1 = sig1 * np.exp(-0.5 * (dw1 / c1)**2) / np.sqrt(2.*np.pi*c1**2)
71+
Hw2 = sig2 * np.exp(-0.5 * (dw2 / c2)**2) / np.sqrt(2.*np.pi*c2**2)
72+
Hw = Hw1 + Hw2
73+
Hw0 = np.concatenate([Hw[0:int(n/2)], Hw[int(n/2):0:-1]])
74+
Sw = (sfrr / 2.) * np.sqrt(Hw0)
75+
ph0 = 2 * np.pi * np.random.uniform(0, 1, int(n/2)-1)
76+
ph = np.concatenate([[0], ph0, [0], -np.flipud(ph0)])
77+
SwC = Sw * np.exp(ph*1j)
78+
x = (1 / float(n)) * np.real(np.fft.ifft(SwC))
79+
xstd = np.std(x)
80+
ratio = rrstd / float(xstd)
81+
return rrmean + x * ratio
82+
83+
def annotate_peaks(x, thetap, sfecg):
84+
"""
85+
This function annotates PQRST peaks (P=1, Q=2, R=3, S=4, T=5).
86+
"""
87+
# find rough positions of peaks
88+
n = len(x)
89+
irpeaks = np.zeros(n)
90+
theta = np.arctan2(x[:,1], x[:,0])
91+
ind0 = np.zeros(n)
92+
for i in range(0, n-1):
93+
a = (theta[i] <= thetap) & (thetap <= theta[i+1])
94+
j = np.where(a==1)[0]
95+
if len(j) > 0:
96+
d1 = thetap[j] - theta[i]
97+
d2 = theta[i+1] - thetap[j]
98+
if d1[0] < d2[0]:
99+
ind0[i] = j[0]+1
100+
else:
101+
ind0[i+1] = j[0]+1
102+
# shift the peaks to correct position
103+
ind = np.zeros(n)
104+
z = x[:,2]
105+
extrema = np.array([np.argmax, np.argmin, np.argmax, np.argmin, np.argmax])
106+
for i in range(0,5):
107+
ind1 = np.where(ind0==i+1)[0]
108+
for j in ind1:
109+
if j:
110+
surrounding = z[j-3:j+3]
111+
correction = extrema[i](surrounding)
112+
ind[j-3+correction] = i+1
113+
return ind
114+
115+
def ecgsyn(sfecg=256, n=256, hrmean=60., hrstd=1,
116+
lfhfratio=0.5, sfint=512, ti=[-70, -15, 0, 15, 100],
117+
ai=[1.2, -5, 30, -7.5, 0.75], bi=[0.25, 0.1, 0.1, 0.1, 0.4]):
118+
"""
119+
ECGSYN - realistic ecg generator.
120+
121+
Kwargs:
122+
123+
* `sfecg` : ECG sampling frequency (int), it Hz
124+
125+
* `N` : approaximate number of heart beats (int)
126+
127+
* `hrmean` : mean heart rate (float) in beats per minute
128+
129+
* `hrstd` : standard deviation of heart rate (float) in beats per minute
130+
131+
* 'lfhfration : LF/HF ratio (float)
132+
133+
* `sfint` : internal sampling frequency (int) in Hz
134+
135+
* `ti` : angles of PQRST extrema (1d array of size 5) in degrees
136+
137+
* `ai` : z-position of PQRST extrema (1d array of size 5)
138+
139+
* `bi` : Gaussian width of peaks (1d array of size 5)
140+
141+
Returns:
142+
143+
* `x` : ECG values in mV
144+
145+
* `peaks`: labels for PQRST peaks (P=1, Q=2, R=3, S=4, T=5 and 0 elsewhere)
146+
147+
"""
148+
# check data types
149+
check_type_or_raise(sfecg, int, "sfecg")
150+
check_type_or_raise(n, int, "sfecg")
151+
check_type_or_raise(sfint, int, "sfint")
152+
# data cleaning
153+
ti = np.array(ti)
154+
hrmean = float(hrmean)
155+
ti = ti * np.pi / 180.
156+
ai = np.array(ai)
157+
bi = np.array(bi)
158+
# adjust extrema parameters for mean heart rate
159+
hrfact = np.sqrt(hrmean / 60.)
160+
hrfact2 = np.sqrt(hrfact)
161+
bi = hrfact * bi
162+
ti = np.array([hrfact2, hrfact, 1, hrfact, hrfact2]) * ti
163+
# check that sfint is an integer multiple of sfecg
164+
if sfint % sfecg != 0 or sfint < sfecg:
165+
raise ValueError("Sfint must be an integer multiple of sfecg")
166+
# define frequency parameters for rr process, flo and fhi correspond
167+
# to the Mayer waves and respiratory rate respectively
168+
flo = 0.1
169+
fhi = 0.25
170+
flostd = 0.01
171+
fhistd = 0.01
172+
# calculate time scales for rr and total output
173+
sampfreqrr = 1
174+
trr = 1 / float(sampfreqrr)
175+
tstep = 1 / float(sfecg)
176+
rrmean = 60 / hrmean
177+
Nrr = 2**(np.ceil(np.log2(n * rrmean / trr)))
178+
# compute rr process
179+
rr0 = rrprocess(flo, fhi, flostd, fhistd, lfhfratio, hrmean, hrstd,
180+
sampfreqrr, Nrr)
181+
# upsample rr time series from 1 Hz to sfint Hz
182+
rrlin = np.arange(0, len(rr0)*sfint) / float(sfint) # upsample
183+
rr = np.interp(rrlin, np.arange(0, len(rr0)), rr0) # upsample
184+
# make the rrn time series
185+
dt = 1 / float(sfint)
186+
rrn = np.zeros(len(rr))
187+
tecg = 0
188+
i = 0
189+
while i <= len(rr):
190+
tecg = tecg + rr[i]
191+
ip = int(np.round(tecg / dt))
192+
rrn[i:ip+1] = rr[i] #+1?
193+
i = ip+1
194+
Nt = ip
195+
# integrate system using fourth order Runge-Kutta
196+
x0 = np.array([1,0,0.04])
197+
Tspan = np.arange(0, (Nt-1)*dt, dt)
198+
Tspan = Tspan[:len(rrn)]
199+
z = ode45(derfunc, Tspan, x0, rrn, sfint, ti, ai, bi)
200+
# resample (downsample)
201+
resample_factor = int(sfint / sfecg)
202+
x = z[::resample_factor]
203+
# annotate peaks
204+
ipeaks = annotate_peaks(x, ti, sfecg)
205+
# Scale signal to lie between -0.4 and 1.2 mV
206+
x = x[:,2]
207+
zmin = x.min()
208+
zmax = x.max()
209+
zrange = zmax - zmin
210+
out = (x - zmin) * 1.6 / zrange - 0.4
211+
return out, ipeaks
212+
213+
if __name__ == "__main__":
214+
x, peaks = ecgsyn(n=10, hrmean=50, hrstd=3, sfecg=128)

signalz/misc.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,34 @@ def check_type_or_raise(obj, expected_type, obj_name):
3232
obj.__class__.__name__)
3333
)
3434

35+
def rem(a, b):
36+
"""
37+
Reminder estimation function (estimates reminder in different way
38+
than it is default in Pyton).
39+
"""
40+
q = abs(a) % b
41+
s = np.sign(a)
42+
return q*s
43+
44+
def ode45_step(f, x, t, dt, *args):
45+
"""
46+
One step of 4th Order Runge-Kutta method
47+
"""
48+
k = dt
49+
k1 = k * f(t, x, *args)
50+
k2 = k * f(t + 0.5*k, x + 0.5*k1, *args)
51+
k3 = k * f(t + 0.5*k, x + 0.5*k2, *args)
52+
k4 = k * f(t + dt, x + k3, *args)
53+
return x + 1/6. * (k1 + k2 + k3 + k4)
54+
55+
def ode45(f, t, x0, *args):
56+
"""
57+
4th Order Runge-Kutta method
58+
"""
59+
n = len(t)
60+
x = np.zeros((n, len(x0)))
61+
x[0] = x0
62+
for i in range(n-1):
63+
dt = t[i+1] - t[i]
64+
x[i+1] = ode45_step(f, x[i], t[i], dt, *args)
65+
return x

0 commit comments

Comments
 (0)