81 lines
2.7 KiB
Swift
81 lines
2.7 KiB
Swift
import Foundation
|
|
import OdeInt
|
|
import RealModule
|
|
|
|
public enum ECGSyn {
|
|
public struct Parameters {
|
|
/// The ECG amplitude in mV.
|
|
let range: (Double, Double) = (-0.4, 1.4)
|
|
|
|
/// Amplitude of the noise.
|
|
let aNoise: Double = 0.0
|
|
|
|
/// The angle of each attractor (P, Q, R, S, T) around the limit cycle, in radians.
|
|
let theta: [Double] = [-70, -15, 0, 15, 100].map { $0 * .pi / 180 }
|
|
|
|
/// The position of attractors (P, Q, R, S, T) above or below the z=0 plane.
|
|
let a: [Double] = [1.2, -5, 30, -7.5, 0.75]
|
|
|
|
/// Widths of the attractors (P, Q, R, S, T).
|
|
let b: [Double] = [0.25, 0.1, 0.1, 0.1, 0.4]
|
|
}
|
|
|
|
public static func generate(params: Parameters, rrSeries: RRSeries<Double>) -> [Double] {
|
|
var rng = rrSeries.rng
|
|
let srInternal = rrSeries.timeParameters.srInternal
|
|
|
|
// adjust extrema parameters for mean heart rate
|
|
let hrFact = sqrt(rrSeries.timeParameters.hrMean / 60.0)
|
|
let hrFactSqrt = sqrt(hrFact)
|
|
|
|
let ai = params.a
|
|
let bi = params.b.map { $0 * hrFact }
|
|
let ti = zip([hrFactSqrt, hrFact, 1, hrFact, hrFactSqrt], params.theta).map(*)
|
|
|
|
let fhi = rrSeries.rrParamaters.fhi
|
|
|
|
let nt = rrSeries.count
|
|
|
|
let dt = 1.0 / Double(srInternal)
|
|
let ts = (0 ..< nt).map { Double($0) * dt }
|
|
let x0 = SIMD3<Double>(1.0, 0.0, 0.04)
|
|
|
|
let result = SIMD3<Double>.integrate(over: ts, y0: x0, tol: 1e-6) { x, t in
|
|
let ta = atan2(x[1], x[0])
|
|
|
|
let r0 = 1.0
|
|
let a0 = 1.0 - sqrt(x[0] * x[0] + x[1] * x[1]) / r0
|
|
|
|
let w0 = 2 * .pi / rrSeries.valueAt(t)
|
|
|
|
let zbase = 0.005 * sin(2 * .pi * fhi * t)
|
|
|
|
var dxdt = SIMD3<Double>(a0 * x[0] - w0 * x[1], a0 * x[1] + w0 * x[0], 0.0)
|
|
|
|
for i in 0 ..< ti.count {
|
|
let dt = remainder(ta - ti[i], 2 * .pi)
|
|
|
|
dxdt[2] += -ai[i] * dt * exp(-0.5 * (dt * dt) / (bi[i] * bi[i]))
|
|
}
|
|
dxdt[2] += -1.0 * (x[2] - zbase)
|
|
|
|
return dxdt
|
|
}
|
|
|
|
// extract z and downsample to ECG sampling frequency
|
|
let qstep = srInternal / rrSeries.timeParameters.srEcg
|
|
var zresult = stride(from: 0, to: nt, by: qstep).map { result[$0][2] }
|
|
|
|
let (zmin, zmax) = zresult.minAndMax()!
|
|
let zrange = zmax - zmin
|
|
|
|
// Scale signal between -0.4 and 1.2 mV
|
|
// add uniformly distributed measurement noise
|
|
for i in 0 ..< zresult.count {
|
|
zresult[i] = (params.range.1 - params.range.0) * (zresult[i] - zmin) / zrange + params.range.0
|
|
zresult[i] += params.aNoise * (2.0 * rng.nextDouble() - 1.0)
|
|
}
|
|
return zresult
|
|
}
|
|
}
|