///|
/// A half-open index range in an RR stream.
pub(all) struct SegmentRange {
start : Int
end : Int
ordinal : Int
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Metrics attached to one analysis segment.
pub(all) struct SegmentSummary {
range : SegmentRange
count : Int
mean_rr : Double
sdnn : Double
rmssd : Double
pnn50 : Double
median_rr : Double
quality_ratio : Double
sd1 : Double
sd2 : Double
} derive(FromJson, ToJson, Debug, Eq)
///|
/// A detected change point between two local windows.
pub(all) struct ChangePoint {
index : Int
before_mean : Double
after_mean : Double
magnitude : Double
confidence : Double
} derive(FromJson, ToJson, Debug, Eq)
///|
/// Build overlapping segment ranges. The final partial segment is omitted.
pub fn make_segment_ranges(
length : Int,
segment_size : Int,
hop_size : Int,
) -> Array[SegmentRange] {
let result = []
if length <= 0 || segment_size <= 0 || hop_size <= 0 || length < segment_size {
return result
}
let mut start = 0
let mut ordinal = 0
while start + segment_size <= length {
result.push({ start, end: start + segment_size, ordinal })
start += hop_size
ordinal += 1
}
result
}
///|
/// Copy one segment from an RR series.
pub fn slice_segment(
values : Array[Double],
range : SegmentRange,
) -> Array[Double] {
let result = []
let start = if range.start < 0 { 0 } else { range.start }
let end = if range.end > values.length() {
values.length()
} else {
range.end
}
if start >= end {
return result
}
for i in start.. Array[SegmentSummary] {
let result = []
let ranges = make_segment_ranges(intervals.length(), segment_size, hop_size)
for range in ranges {
let segment = slice_segment(intervals, range)
let validation = validate_intervals(segment, config)
let poincare = calculate_poincare(segment)
result.push({
range,
count: segment.length(),
mean_rr: mean_value(segment),
sdnn: calculate_sdnn(segment),
rmssd: calculate_rmssd(segment),
pnn50: calculate_pnn(segment, 50.0),
median_rr: median_value(segment),
quality_ratio: if segment.length() == 0 {
0.0
} else {
validation.valid.to_double() / segment.length().to_double()
},
sd1: poincare.sd1,
sd2: poincare.sd2,
})
}
result
}
///|
/// Calculate a causal rolling RMSSD series.
pub fn rolling_rmssd(
intervals : Array[Double],
window_size : Int,
) -> Array[Double] {
let result = []
if window_size <= 0 {
return result
}
for i in 0.. Array[Double] {
let result = []
if window_size <= 0 {
return result
}
for i in 0.. Array[ChangePoint] {
let result = []
if window_size <= 0 || values.length() < window_size * 2 {
return result
}
let mut index = window_size
while index + window_size <= values.length() {
let before = []
let after = []
for i in (index - window_size)..= minimum_magnitude {
let scale = standard_deviation(before) + standard_deviation(after)
let confidence = if scale == 0.0 {
1.0
} else {
(magnitude / scale).clamp(min=0.0, max=1.0)
}
result.push({ index, before_mean, after_mean, magnitude, confidence })
}
index += window_size
}
result
}
///|
/// A small private absolute-value helper for change-point calculations.
fn absolute_difference(left : Double, right : Double) -> Double {
let difference = left - right
if difference < 0.0 {
-difference
} else {
difference
}
}
///|
/// Downsample a sequence by averaging fixed-size blocks.
pub fn downsample_mean(
values : Array[Double],
block_size : Int,
) -> Array[Double] {
let result = []
if block_size <= 0 {
return result
}
let mut start = 0
while start < values.length() {
let stop = if start + block_size > values.length() {
values.length()
} else {
start + block_size
}
let block = []
for i in start.. Array[Array[Double]] {
let result = []
let mut current = []
for interval in intervals {
if interval > gap_threshold_ms && current.length() > 0 {
result.push(current)
current = []
}
current.push(interval)
}
if current.length() > 0 {
result.push(current)
}
result
}
///|
/// Return the largest contiguous run after removing invalid intervals.
pub fn longest_valid_segment(
intervals : Array[Double],
config : HrvConfig,
) -> Array[Double] {
let mut best = []
let mut current = []
for interval in intervals {
if classify_interval(interval, config) is Normal {
current.push(interval)
if current.length() > best.length() {
best = []
for value in current {
best.push(value)
}
}
} else {
current = []
}
}
best
}
///|
/// Calculate a quality-weighted mean of segment RMSSD values.
pub fn weighted_segment_rmssd(summaries : Array[SegmentSummary]) -> Double {
let values = []
let weights = []
for summary in summaries {
values.push(summary.rmssd)
weights.push(summary.quality_ratio)
}
weighted_mean(values, weights)
}