///|
/// A uniformly sampled tachogram used by spectral analysis.
pub(all) struct Tachogram {
sample_rate_hz : Double
timestamps_s : Array[Double]
values_ms : Array[Double]
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Window functions for periodogram input.
pub(all) enum WindowFunction {
Rectangular
Hann
Hamming
Blackman
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Convert RR durations into cumulative beat timestamps.
pub fn rr_timestamps(intervals : Array[Double]) -> Array[Double] {
let result = []
let mut elapsed = 0.0
for interval in intervals {
elapsed += interval / 1000.0
result.push(elapsed)
}
result
}
///|
/// Linearly interpolate a value at a timestamp, clamping at the endpoints.
pub fn interpolate_at(
timestamps : Array[Double],
values : Array[Double],
timestamp : Double,
) -> Double {
let n = if timestamps.length() < values.length() {
timestamps.length()
} else {
values.length()
}
if n == 0 {
return 0.0
}
if n == 1 || timestamp <= timestamps[0] {
return values[0]
}
if timestamp >= timestamps[n - 1] {
return values[n - 1]
}
let mut right = 1
while right < n && timestamps[right] < timestamp {
right += 1
}
let left = right - 1
let width = timestamps[right] - timestamps[left]
if width <= 0.0 {
values[left]
} else {
let fraction = (timestamp - timestamps[left]) / width
values[left] + fraction * (values[right] - values[left])
}
}
///|
/// Resample the instantaneous RR series onto an evenly spaced time grid.
pub fn resample_rr(
intervals : Array[Double],
sample_rate_hz : Double,
) -> Tachogram {
if intervals.length() == 0 || sample_rate_hz <= 0.0 {
return { sample_rate_hz, timestamps_s: [], values_ms: [] }
}
let timestamps = rr_timestamps(intervals)
let duration = timestamps[timestamps.length() - 1]
let count = (duration * sample_rate_hz).floor().to_int() + 1
let output_timestamps = []
let output_values = []
for i in 0.. Array[Double] {
let trend = fit_linear_trend(values)
let result = []
for i in 0.. Double {
if length <= 1 {
return 1.0
}
let phase = 2.0 * @math.PI * index.to_double() / (length - 1).to_double()
match function {
Rectangular => 1.0
Hann => 0.5 * (1.0 - @math.cos(phase))
Hamming => 0.54 - 0.46 * @math.cos(phase)
Blackman => 0.42 - 0.5 * @math.cos(phase) + 0.08 * @math.cos(2.0 * phase)
}
}
///|
/// Apply a window without changing the input array.
pub fn apply_window(
values : Array[Double],
function : WindowFunction,
) -> Array[Double] {
let result = []
for i in 0.. Array[Double] {
let centered = []
let average = mean_value(values)
for value in values {
centered.push(value - average)
}
let detrended = if remove_trend { detrend_linear(centered) } else { centered }
apply_window(detrended, function)
}
///|
/// Split a sequence into overlapping frames for streaming or spectral work.
pub fn frame_values(
values : Array[Double],
frame_size : Int,
hop_size : Int,
) -> Array[Array[Double]] {
let result = []
if frame_size <= 0 || hop_size <= 0 || values.length() < frame_size {
return result
}
let mut start = 0
while start + frame_size <= values.length() {
let frame = []
for i in start..<(start + frame_size) {
frame.push(values[i])
}
result.push(frame)
start += hop_size
}
result
}
///|
/// Resample, center, detrend, and window an RR series in one call.
pub fn prepare_tachogram(
intervals : Array[Double],
sample_rate_hz : Double,
remove_trend : Bool,
function : WindowFunction,
) -> Tachogram {
let tachogram = resample_rr(intervals, sample_rate_hz)
let values = prepare_spectral_values(
tachogram.values_ms,
remove_trend,
function,
)
{
sample_rate_hz: tachogram.sample_rate_hz,
timestamps_s: tachogram.timestamps_s,
values_ms: values,
}
}