///|
/// Features computed for one timestamped signal window.
pub struct WindowFeature {
start_idx : Int
end_idx : Int
duration_s : Double
summary : SignalSummary
area : Double
peak_index : Int
time_to_peak_s : Double
slope_to_peak : Double
zero_crossings : Int
} derive(Debug, Eq)
///|
fn count_crossings(values : Array[Double], center : Double) -> Int {
let mut previous = 0
let mut count = 0
for value in values {
if !value.is_nan() && !value.is_inf() {
let sign = if value > center {
1
} else if value < center {
-1
} else {
0
}
if sign != 0 {
if previous != 0 && sign != previous {
count = count + 1
}
previous = sign
}
}
}
count
}
///|
/// Computes integral, extrema, timing, slope, and crossing features safely.
pub fn compute_window_features(
timestamps : Array[Double],
values : Array[Double],
start_idx : Int,
end_idx : Int,
) -> WindowFeature {
let common = if timestamps.length() < values.length() {
timestamps.length()
} else {
values.length()
}
let range = clamp_range(common, start_idx, end_idx)
let summary = summarize_signal(copy_range(values, range.start, range.end))
if range.start >= range.end {
return {
start_idx: range.start,
end_idx: range.end,
duration_s: 0.0,
summary,
area: 0.0,
peak_index: -1,
time_to_peak_s: 0.0,
slope_to_peak: 0.0,
zero_crossings: 0,
}
}
let end_time = if range.end < timestamps.length() {
timestamps[range.end]
} else {
timestamps[timestamps.length() - 1]
}
let duration = end_time - timestamps[range.start]
let mut area = 0.0
for i = range.start; i < range.end - 1; i = i + 1 {
let dt = timestamps[i + 1] - timestamps[i]
if dt > 0.0 &&
!dt.is_nan() &&
!dt.is_inf() &&
!values[i].is_nan() &&
!values[i + 1].is_nan() &&
!values[i].is_inf() &&
!values[i + 1].is_inf() {
area = area + (values[i] + values[i + 1]) * dt / 2.0
}
}
let mut peak_index = range.start
for i = range.start + 1; i < range.end; i = i + 1 {
if values[i] > values[peak_index] {
peak_index = i
}
}
let peak_time = timestamps[peak_index] - timestamps[range.start]
let slope = if peak_time > 0.0 {
(values[peak_index] - values[range.start]) / peak_time
} else {
0.0
}
let window = copy_range(values, range.start, range.end)
{
start_idx: range.start,
end_idx: range.end,
duration_s: if duration > 0.0 {
duration
} else {
0.0
},
summary,
area,
peak_index,
time_to_peak_s: if peak_time > 0.0 {
peak_time
} else {
0.0
},
slope_to_peak: slope,
zero_crossings: count_crossings(window, summary.mean),
}
}
///|
/// Computes overlapping signal windows with deterministic tail handling.
pub fn compute_sliding_signal_features(
timestamps : Array[Double],
values : Array[Double],
window_samples : Int,
step_samples : Int,
) -> Array[WindowFeature] {
let common = if timestamps.length() < values.length() {
timestamps.length()
} else {
values.length()
}
if common < 2 || window_samples <= 0 || step_samples <= 0 {
return []
}
let result : Array[WindowFeature] = []
let mut start = 0
while start < common {
let end = if start + window_samples > common {
common
} else {
start + window_samples
}
if end - start < 2 {
break
}
result.push(compute_window_features(timestamps, values, start, end))
start = start + step_samples
}
result
}