///|
/// Parameters for robust one-dimensional signal processing.
pub struct SignalRule {
window : Int
threshold : Double
smoothing : Double
minimum_gap : Int
}
///|
pub struct SignalPeak {
index : Int
value : Double
prominence : Double
left : Double
right : Double
}
///|
pub struct SignalChange {
index : Int
before : Double
after : Double
score : Double
}
///|
pub fn signal_rule(
window : Int,
threshold : Double,
smoothing : Double,
minimum_gap : Int,
) -> SignalRule {
{
window: if window < 1 {
1
} else {
window
},
threshold: if threshold < 0.0 {
-threshold
} else {
threshold
},
smoothing: if smoothing < 0.0 {
0.0
} else if smoothing > 1.0 {
1.0
} else {
smoothing
},
minimum_gap: if minimum_gap < 1 {
1
} else {
minimum_gap
},
}
}
///|
pub fn signal_identity(data : Array[Double]) -> Array[Double] {
data.copy()
}
///|
pub fn signal_convolution(
data : Array[Double],
kernel : Array[Double],
) -> Array[Double] {
let result = []
if data.length() == 0 || kernel.length() == 0 {
return result
}
let radius = kernel.length() / 2
for index = 0; index < data.length(); index = index + 1 {
let mut total = 0.0
let mut weight = 0.0
for offset = 0; offset < kernel.length(); offset = offset + 1 {
let source = index + offset - radius
let bounded = if source < 0 {
0
} else if source >= data.length() {
data.length() - 1
} else {
source
}
total += data[bounded] * kernel[offset]
weight += kernel[offset]
}
result.push(if weight == 0.0 { 0.0 } else { total / weight })
}
result
}
///|
pub fn signal_box_kernel(window : Int) -> Array[Double] {
let width = if window < 1 { 1 } else { window }
let result = []
for _ in 0.. Array[Double] {
signal_convolution(data, signal_box_kernel(window))
}
///|
pub fn signal_moving_median(
data : Array[Double],
window : Int,
) -> Array[Double] {
let result = []
let width = if window < 1 { 1 } else { window }
let radius = width / 2
for index = 0; index < data.length(); index = index + 1 {
let values = []
let start = if index < radius { 0 } else { index - radius }
let end = if index + radius + 1 > data.length() {
data.length()
} else {
index + radius + 1
}
for cursor = start; cursor < end; cursor = cursor + 1 {
values.push(data[cursor])
}
result.push(median(values))
}
result
}
///|
pub fn signal_exponential_smooth(
data : Array[Double],
alpha : Double,
) -> Array[Double] {
streaming_ewma(data, alpha)
}
///|
pub fn signal_double_smooth(
data : Array[Double],
alpha : Double,
) -> Array[Double] {
signal_exponential_smooth(signal_exponential_smooth(data, alpha), alpha)
}
///|
pub fn signal_first_difference(data : Array[Double]) -> Array[Double] {
transform_difference(data, 1)
}
///|
pub fn signal_second_difference(data : Array[Double]) -> Array[Double] {
signal_first_difference(signal_first_difference(data))
}
///|
pub fn signal_gradient(data : Array[Double]) -> Array[Double] {
let result = []
if data.length() == 0 {
return result
}
result.push(0.0)
for index = 1; index < data.length(); index = index + 1 {
result.push(data[index] - data[index - 1])
}
result
}
///|
pub fn signal_total_variation(data : Array[Double]) -> Double {
sum_absolute(signal_first_difference(data))
}
///|
pub fn signal_energy(data : Array[Double]) -> Double {
sum_squared(data)
}
///|
pub fn signal_root_mean_square(data : Array[Double]) -> Double {
if data.length() == 0 {
0.0
} else {
(signal_energy(data) / data.length().to_double()).sqrt()
}
}
///|
pub fn signal_zero_crossings(data : Array[Double]) -> Int {
if data.length() < 2 {
return 0
}
let mut count = 0
for index = 1; index < data.length(); index = index + 1 {
if (data[index] < 0.0 && data[index - 1] >= 0.0) ||
(data[index] >= 0.0 && data[index - 1] < 0.0) {
count += 1
}
}
count
}
///|
pub fn signal_autocorrelation(data : Array[Double], lag : Int) -> Double {
let safe_lag = if lag < 0 { 0 } else { lag }
if data.length() <= safe_lag || data.length() == 0 {
return 0.0
}
let center = mean(data)
let mut numerator = 0.0
let mut denominator = 0.0
for index = 0; index < data.length(); index = index + 1 {
let deviation = data[index] - center
denominator += deviation * deviation
if index + safe_lag < data.length() {
numerator += deviation * (data[index + safe_lag] - center)
}
}
if denominator == 0.0 {
0.0
} else {
numerator / denominator
}
}
///|
pub fn signal_autocorrelation_path(
data : Array[Double],
maximum_lag : Int,
) -> Array[Double] {
let result = []
let count = if maximum_lag < 0 { 0 } else { maximum_lag }
for lag = 0; lag <= count; lag = lag + 1 {
result.push(signal_autocorrelation(data, lag))
}
result
}
///|
pub fn signal_peak_candidates(
data : Array[Double],
threshold : Double,
) -> Array[SignalPeak] {
let result = []
if data.length() < 3 {
return result
}
let baseline = median(data)
let scale = mad(data)
let safe_threshold = if threshold < 0.0 { -threshold } else { threshold }
for index = 1; index < data.length() - 1; index = index + 1 {
let left = data[index - 1]
let value = data[index]
let right = data[index + 1]
let prominence = value - (left + right) / 2.0
if value >= left &&
value >= right &&
prominence > safe_threshold * (if scale == 0.0 { 1.0 } else { scale }) &&
value > baseline {
result.push({ index, value, prominence, left, right })
}
}
result
}
///|
pub fn signal_trough_candidates(
data : Array[Double],
threshold : Double,
) -> Array[SignalPeak] {
let inverted = []
for value in data {
inverted.push(-value)
}
let peaks = signal_peak_candidates(inverted, threshold)
let result = []
for peak in peaks {
result.push({
..peak,
value: -peak.value,
left: -peak.left,
right: -peak.right,
prominence: peak.prominence,
})
}
result
}
///|
pub fn signal_peak_indices(peaks : Array[SignalPeak]) -> Array[Int] {
let result = []
for peak in peaks {
result.push(peak.index)
}
result
}
///|
pub fn signal_peak_values(peaks : Array[SignalPeak]) -> Array[Double] {
let result = []
for peak in peaks {
result.push(peak.value)
}
result
}
///|
pub fn signal_local_range(data : Array[Double], window : Int) -> Array[Double] {
let result = []
let width = if window < 1 { 1 } else { window }
for index = 0; index < data.length(); index = index + 1 {
let start = if index + 1 > width { index + 1 - width } else { 0 }
let values = []
for cursor = start; cursor <= index; cursor = cursor + 1 {
values.push(data[cursor])
}
result.push(range(values))
}
result
}
///|
pub fn signal_local_scale(data : Array[Double], window : Int) -> Array[Double] {
let result = []
let width = if window < 1 { 1 } else { window }
for index = 0; index < data.length(); index = index + 1 {
let start = if index + 1 > width { index + 1 - width } else { 0 }
let values = []
for cursor = start; cursor <= index; cursor = cursor + 1 {
values.push(data[cursor])
}
result.push(mad(values))
}
result
}
///|
pub fn signal_local_z(data : Array[Double], window : Int) -> Array[Double] {
transform_rolling_z(data, window)
}
///|
pub fn signal_change_candidates(
data : Array[Double],
window : Int,
threshold : Double,
) -> Array[SignalChange] {
let result = []
let width = if window < 1 { 1 } else { window }
if data.length() <= width * 2 {
return result
}
let safe_threshold = if threshold < 0.0 { -threshold } else { threshold }
for index = width; index < data.length() - width; index = index + 1 {
let before = []
let after = []
for cursor = index - width; cursor < index; cursor = cursor + 1 {
before.push(data[cursor])
}
for cursor = index; cursor < index + width; cursor = cursor + 1 {
after.push(data[cursor])
}
let difference = abs_double(mean(after) - mean(before))
let scale = (mad(before) + mad(after)) / 2.0
let score = if scale == 0.0 { difference } else { difference / scale }
if score >= safe_threshold {
result.push({ index, before: mean(before), after: mean(after), score })
}
}
result
}
///|
pub fn signal_change_indices(changes : Array[SignalChange]) -> Array[Int] {
let result = []
for change in changes {
result.push(change.index)
}
result
}
///|
pub fn signal_change_scores(changes : Array[SignalChange]) -> Array[Double] {
let result = []
for change in changes {
result.push(change.score)
}
result
}
///|
pub fn signal_denoise(data : Array[Double], rule : SignalRule) -> Array[Double] {
let median_smoothed = signal_moving_median(data, rule.window)
let average_smoothed = signal_moving_average(median_smoothed, rule.window)
let result = []
for index = 0; index < data.length(); index = index + 1 {
result.push(
rule.smoothing * average_smoothed[index] +
(1.0 - rule.smoothing) * data[index],
)
}
result
}
///|
pub fn signal_noise_estimate(data : Array[Double]) -> Double {
mad(signal_first_difference(data)) / 1.4826
}
///|
pub fn signal_snr(data : Array[Double]) -> Double {
let noise = signal_noise_estimate(data)
if noise == 0.0 {
0.0
} else {
signal_root_mean_square(data) / noise
}
}
///|
pub fn signal_outlier_repaired(
data : Array[Double],
threshold : Double,
) -> Array[Double] {
let smoothed = signal_moving_median(data, 3)
let scale = signal_noise_estimate(data)
let safe_scale = if scale == 0.0 { 1.0 } else { scale }
let result = []
for index = 0; index < data.length(); index = index + 1 {
if abs_double(data[index] - smoothed[index]) > threshold * safe_scale {
result.push(smoothed[index])
} else {
result.push(data[index])
}
}
result
}
///|
pub fn signal_resample_linear(
data : Array[Double],
target_size : Int,
) -> Array[Double] {
let result = []
let count = if target_size < 0 { 0 } else { target_size }
if count == 0 || data.length() == 0 {
return result
}
if data.length() == 1 {
for _ in 0..= data.length() { left } else { left + 1 }
let fraction = position - left.to_double()
result.push(data[left] + fraction * (data[right] - data[left]))
}
result
}
///|
pub fn signal_quantize(data : Array[Double], levels : Int) -> Array[Double] {
let result = []
let count = if levels < 2 { 2 } else { levels }
let minimum = min_value(data)
let maximum = max_value(data)
let width = maximum - minimum
for value in data {
if width == 0.0 {
result.push(value)
} else {
let index = ((value - minimum) / width * (count - 1).to_double()).to_int()
result.push(minimum + index.to_double() / (count - 1).to_double() * width)
}
}
result
}
///|
pub fn signal_run_lengths(
data : Array[Double],
tolerance : Double,
) -> Array[Int] {
let result = []
if data.length() == 0 {
return result
}
let mut run = 1
for index = 1; index < data.length(); index = index + 1 {
if abs_double(data[index] - data[index - 1]) <= tolerance {
run += 1
} else {
result.push(run)
run = 1
}
}
result.push(run)
result
}
///|
pub fn signal_flatline_fraction(
data : Array[Double],
tolerance : Double,
) -> Double {
let runs = signal_run_lengths(data, tolerance)
let mut repeated = 0
for run in runs {
if run > 1 {
repeated += run
}
}
if data.length() == 0 {
0.0
} else {
repeated.to_double() / data.length().to_double()
}
}
///|
pub fn signal_summary(data : Array[Double], window : Int) -> Array[Double] {
[
signal_energy(data),
signal_root_mean_square(data),
signal_total_variation(data),
signal_noise_estimate(data),
signal_snr(data),
signal_flatline_fraction(data, 0.0),
signal_autocorrelation(data, 1),
signal_autocorrelation(data, window),
]
}