///|
/// Cleaning methods for RR intervals.
pub(all) enum CleaningMethod {
Remove
InterpolateLocalMedian
InterpolateLinear
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Clean RR intervals and generate a quality report.
pub fn clean_rr_intervals(
intervals : Array[Double],
cleaning_method : CleaningMethod,
config : HrvConfig,
) -> (Array[Double], QualityReport) {
let n = intervals.length()
let is_ectopic = Array::make(n, false)
let mut ectopic_count = 0
for i in 0.. config.max_rr {
is_ectopic[i] = true
ectopic_count += 1
} else {
match get_local_median(intervals, i, config.min_rr, config.max_rr) {
Some(med) => {
let diff = val - med
let abs_diff = if diff < 0.0 { -diff } else { diff }
if abs_diff / med > config.relative_threshold {
is_ectopic[i] = true
ectopic_count += 1
}
}
None => ()
}
}
}
let clean_ratio = if n == 0 {
0.0
} else {
(n - ectopic_count).to_double() / n.to_double()
}
let is_low_quality = clean_ratio < 0.85
let quality : QualityReport = {
total_beats: n,
valid_beats: n - ectopic_count,
ectopic_beats: ectopic_count,
clean_ratio,
is_low_quality,
}
match cleaning_method {
Remove => {
let result = []
for i in 0.. {
let result = Array::make(n, 0.0)
for i in 0.. result[i] = med
None => result[i] = find_nearest_valid(intervals, is_ectopic, i)
}
}
}
(result, quality)
}
InterpolateLinear => {
let result = Array::make(n, 0.0)
for i in 0.. {
let fraction = (i - l_idx).to_double() /
(r_idx - l_idx).to_double()
result[i] = l_val + fraction * (r_val - l_val)
}
(Some((_, l_val)), None) => result[i] = l_val
(None, Some((_, r_val))) => result[i] = r_val
(None, None) => result[i] = 1000.0
}
}
}
(result, quality)
}
}
}
///|
fn get_local_median(
intervals : Array[Double],
index : Int,
min_rr : Double,
max_rr : Double,
) -> Double? {
let window = []
for offset in -2..<=2 {
let idx = index + offset
if idx >= 0 && idx < intervals.length() {
let val = intervals[idx]
if val >= min_rr && val <= max_rr {
window.push(val)
}
}
}
if window.length() > 0 {
window.sort()
let mid = window.length() / 2
if window.length() % 2 == 1 {
Some(window[mid])
} else {
Some((window[mid - 1] + window[mid]) / 2.0)
}
} else {
None
}
}
///|
fn find_nearest_valid(
intervals : Array[Double],
is_ectopic : Array[Bool],
index : Int,
) -> Double {
let mut left = index - 1
let mut right = index + 1
let n = intervals.length()
for ;; {
if left >= 0 && !is_ectopic[left] {
break intervals[left]
}
if right < n && !is_ectopic[right] {
break intervals[right]
}
if left < 0 && right >= n {
break 1000.0
}
left -= 1
right += 1
}
}
///|
fn find_left_valid(
intervals : Array[Double],
is_ectopic : Array[Bool],
index : Int,
) -> (Int, Double)? {
let mut i = index - 1
while i >= 0 {
if !is_ectopic[i] {
return Some((i, intervals[i]))
}
i -= 1
}
None
}
///|
fn find_right_valid(
intervals : Array[Double],
is_ectopic : Array[Bool],
index : Int,
) -> (Int, Double)? {
let mut i = index + 1
let n = intervals.length()
while i < n {
if !is_ectopic[i] {
return Some((i, intervals[i]))
}
i += 1
}
None
}