///|
/// Flatten a 2D reprojection residual into a solver block.
pub fn residual_block_2d(
  predicted : @core.Point2,
  observed : @core.Point2,
) -> Array[Double] {
  let difference = predicted.minus(observed)
  [difference.x, difference.y]
}

///|
pub fn residual_norm_2d(
  predicted : @core.Point2,
  observed : @core.Point2,
) -> Double {
  predicted.minus(observed).norm()
}

///|
pub fn residual_blocks_2d(
  predicted : ArrayView[@core.Point2],
  observed : ArrayView[@core.Point2],
) -> Array[Double] {
  let result : Array[Double] = []
  let count = predicted.length().min(observed.length())
  for i in 0.. Array[Double] {
  let result : Array[Double] = []
  let count = predicted.length().min(observed.length())
  for i in 0.. Array[Double] {
  let difference = predicted.minus(observed)
  [difference.x, difference.y, difference.z]
}

///|
pub fn residual_norm_3d(
  predicted : @core.Point3,
  observed : @core.Point3,
) -> Double {
  predicted.minus(observed).norm()
}

///|
pub fn residual_blocks_3d(
  predicted : ArrayView[@core.Point3],
  observed : ArrayView[@core.Point3],
) -> Array[Double] {
  let result : Array[Double] = []
  let count = predicted.length().min(observed.length())
  for i in 0.. Double {
  let count = residuals.length().min(weights.length())
  let scale = loss_scale.abs().max(0.000000000001)
  let mut cost = 0.0
  for i in 0.. Double {
  let divisor = scale.abs().max(0.000000000001)
  let mut cost = 0.0
  for residual in residuals {
    let normalized = residual / divisor
    cost += @math.ln(1.0 + normalized * normalized)
  }
  cost
}

///|
pub fn charbonnier_residual_cost(
  residuals : ArrayView[Double],
  epsilon : Double,
) -> Double {
  let e = epsilon.abs().max(0.000000000001)
  let mut cost = 0.0
  for residual in residuals {
    cost += (residual * residual + e * e).sqrt()
  }
  cost
}

///|
pub fn residual_weights_huber(
  residuals : ArrayView[Double],
  scale : Double,
) -> Array[Double] {
  let result : Array[Double] = []
  let threshold = scale.abs().max(0.000000000001)
  for residual in residuals {
    let value = residual.abs()
    result.push(if value <= threshold { 1.0 } else { threshold / value })
  }
  result
}

///|
pub fn residual_weights_tukey(
  residuals : ArrayView[Double],
  scale : Double,
) -> Array[Double] {
  let result : Array[Double] = []
  let threshold = scale.abs().max(0.000000000001)
  for residual in residuals {
    let normalized = residual / threshold
    if normalized.abs() < 1.0 {
      let factor = 1.0 - normalized * normalized
      result.push(factor * factor)
    } else {
      result.push(0.0)
    }
  }
  result
}

///|
pub fn residual_inlier_mask(
  residuals : ArrayView[Double],
  threshold : Double,
) -> Array[Bool] {
  let result : Array[Bool] = []
  for residual in residuals {
    result.push(residual.abs() <= threshold)
  }
  result
}

///|
pub fn residual_inlier_count(
  residuals : ArrayView[Double],
  threshold : Double,
) -> Int {
  let mut count = 0
  for residual in residuals {
    if residual.abs() <= threshold {
      count += 1
    }
  }
  count
}

///|
pub fn residual_inlier_ratio2(
  residuals : ArrayView[Double],
  threshold : Double,
) -> Double {
  if residuals.length() == 0 {
    0.0
  } else {
    residual_inlier_count(residuals, threshold).to_double() /
    residuals.length().to_double()
  }
}

///|
pub fn residual_maximum(residuals : ArrayView[Double]) -> Double {
  let mut maximum = 0.0
  for residual in residuals {
    maximum = maximum.max(residual.abs())
  }
  maximum
}

///|
pub fn residual_mean_absolute(residuals : ArrayView[Double]) -> Double {
  if residuals.length() == 0 {
    0.0
  } else {
    let mut sum = 0.0
    for residual in residuals {
      sum += residual.abs()
    }
    sum / residuals.length().to_double()
  }
}

///|
pub fn residual_rms2(residuals : ArrayView[Double]) -> Double {
  if residuals.length() == 0 {
    0.0
  } else {
    let mut sum = 0.0
    for residual in residuals {
      sum += residual * residual
    }
    (sum / residuals.length().to_double()).sqrt()
  }
}