///|
/// Feature extraction for motion signals and benchmark diagnostics.
pub struct SignalPeak2 {
  index : Int
  value : Double
  prominence : Double
  width : Int
} derive(Debug)

///|
pub fn SignalPeak2::index(self : SignalPeak2) -> Int {
  self.index
}

///|
pub fn SignalPeak2::value(self : SignalPeak2) -> Double {
  self.value
}

///|
pub fn SignalPeak2::prominence(self : SignalPeak2) -> Double {
  self.prominence
}

///|
pub fn SignalPeak2::width(self : SignalPeak2) -> Int {
  self.width
}

///|
pub fn moving_average2(values : Array[Double], radius : Int) -> Array[Double] {
  let result : Array[Double] = []
  let window = radius.max(0)
  for index in 0.. Array[Double] {
  if values.length() == 0 {
    return []
  }
  let result : Array[Double] = [values[0]]
  let alpha = clamp01(factor)
  for index in 1.. Array[Double] {
  let result : Array[Double] = []
  let window = radius.max(0)
  for index in 0.. (Array[Double], Array[Double]) {
  let lower : Array[Double] = []
  let upper : Array[Double] = []
  let window = radius.max(0)
  for index in 0.. Array[SignalPeak2] {
  let result : Array[SignalPeak2] = []
  if values.length() < 3 {
    return result
  }
  for index in 1..<(values.length() - 1) {
    if values[index] >= threshold &&
      values[index] >= values[index - 1] &&
      values[index] >= values[index + 1] {
      let allowed = if result.length() == 0 {
        true
      } else {
        index - result[result.length() - 1].index >= minimum_distance.max(1)
      }
      if allowed {
        let prominence = values[index] -
          values[index - 1].min(values[index + 1])
        result.push({ index, value: values[index], prominence, width: 1 })
      } else if values[index] > result[result.length() - 1].value {
        result[result.length() - 1] = {
          index,
          value: values[index],
          prominence: values[index] - values[index - 1].min(values[index + 1]),
          width: 1,
        }
      }
    }
  }
  result
}

///|
pub fn signal_rms2(values : Array[Double]) -> Double {
  if values.length() == 0 {
    return 0.0
  }
  let mut sum = 0.0
  for value in values {
    sum = sum + value * value
  }
  @math.pow(sum / values.length().to_double(), 0.5)
}

///|
pub fn signal_mean2(values : Array[Double]) -> Double {
  if values.length() == 0 {
    return 0.0
  }
  values.fold(init=0.0, fn(sum, value) { sum + value }) /
  values.length().to_double()
}

///|
pub fn signal_variance2(values : Array[Double]) -> Double {
  if values.length() == 0 {
    return 0.0
  }
  let mean = signal_mean2(values)
  values.fold(init=0.0, fn(sum, value) {
    let delta = value - mean
    sum + delta * delta
  }) /
  values.length().to_double()
}

///|
pub fn signal_autocorrelation2(values : Array[Double], lag : Int) -> Double {
  let offset = lag.max(0)
  if offset >= values.length() {
    return 0.0
  }
  let mean = signal_mean2(values)
  let mut numerator = 0.0
  let mut denominator = 0.0
  for index in 0..<(values.length() - offset) {
    numerator = numerator +
      (values[index] - mean) * (values[index + offset] - mean)
  }
  for value in values {
    let delta = value - mean
    denominator = denominator + delta * delta
  }
  if denominator == 0.0 {
    0.0
  } else {
    numerator / denominator
  }
}

///|
pub fn signal_normalize2(values : Array[Double]) -> Array[Double] {
  let mean = signal_mean2(values)
  let deviation = @math.pow(signal_variance2(values), 0.5)
  let result : Array[Double] = []
  for value in values {
    result.push(if deviation == 0.0 { 0.0 } else { (value - mean) / deviation })
  }
  result
}

///|
pub fn signal_residual2(
  values : Array[Double],
  smooth_radius : Int,
) -> Array[Double] {
  let smooth = moving_average2(values, smooth_radius)
  let result : Array[Double] = []
  for index in 0.. Array[Double] {
  let result : Array[Double] = []
  let dt = 1.0 / frame_rate.max(0.001)
  for index in 0.. Array[Double] {
  let result : Array[Double] = [initial]
  let dt = 1.0 / frame_rate.max(0.001)
  for index in 1..