///|
/// A small external-model example: y_hat[t] = gain * sensor[t] + offset.
/// The model parameters are outside WarpFit. Its input gradient supplies the
/// chain rule even when prediction and target have different sample counts.
fn objective(
  sensor : Array[Double],
  gain : Double,
  offset : Double,
  target : @warpfit.Series,
) -> @warpfit.Divergence {
  let prediction : @warpfit.Series = {
    count: sensor.length(),
    dimensions: 1,
    values: sensor.map(v => gain * v + offset),
  }
  @warpfit.divergence(prediction, target, 0.3).unwrap()
}

///|
fn model_gradient(
  sensor : Array[Double],
  result : @warpfit.Divergence,
) -> (Double, Double) {
  let mut gain_gradient = 0.0
  let mut offset_gradient = 0.0
  for i, v in result.gradient_x {
    gain_gradient += v * sensor[i]
    offset_gradient += v
  }
  (gain_gradient, offset_gradient)
}

///|
fn main raise {
  let sensor = [0.0, 0.4, 0.8, 1.0, 0.3, 0.0]
  let target : @warpfit.Series = {
    count: 4,
    dimensions: 1,
    values: [0.0, 1.2, 0.7, 0.0],
  }
  let mut gain = 0.5
  let mut offset = 0.6
  let mut result = objective(sensor, gain, offset, target)
  let initial = result.value
  for _iteration = 0; _iteration < 80; _iteration = _iteration + 1 {
    let (dg, db) = model_gradient(sensor, result)
    let mut step = 0.1
    for _attempt = 0; _attempt < 20; _attempt = _attempt + 1 {
      let next = objective(sensor, gain - step * dg, offset - step * db, target)
      if next.value <= result.value {
        gain -= step * dg
        offset -= step * db
        result = next
        break
      }
      step *= 0.5
    }
  }
  assert_true(result.value < initial)
  println("calibration_initial=\{initial}")
  println("calibration_final=\{result.value}")
  println("gain=\{gain}")
  println("offset=\{offset}")
}