///|
pub fn RansacConfig::validated(
  config : RansacConfig,
  data_size~ : Int,
) -> RansacConfig raise @core.GeometryError {
  if data_size <= 0 {
    raise @core.GeometryError::NotEnoughPoints("RANSAC data must not be empty")
  }
  if config.iterations <= 0 {
    raise @core.GeometryError::DegenerateInput(
      "RANSAC iterations must be positive",
    )
  }
  if config.threshold <= 0.0 {
    raise @core.GeometryError::DegenerateInput(
      "RANSAC threshold must be positive",
    )
  }
  if config.min_inliers < 1 || config.min_inliers > data_size {
    raise @core.GeometryError::DegenerateInput(
      "RANSAC min_inliers is outside data size",
    )
  }
  config
}

///|
pub fn InlierMask::count(mask : InlierMask) -> Int {
  let mut result = 0
  for value in mask.values {
    if value {
      result += 1
    }
  }
  result
}

///|
pub fn InlierMask::indices(mask : InlierMask) -> Array[Int] {
  let result : Array[Int] = []
  for i in 0.. Int {
  result.inliers.length() - result.inlier_count
}

///|
pub fn[T] RansacResult::is_confident(
  result : RansacResult[T],
  minimum_ratio? : Double = 0.5,
) -> Bool {
  result.inlier_ratio() >= minimum_ratio
}

///|
pub fn adaptive_iteration_bound(
  inlier_ratio~ : Double,
  sample_size~ : Int,
  confidence? : Double = 0.99,
) -> Int raise @core.GeometryError {
  if inlier_ratio <= 0.0 ||
    inlier_ratio > 1.0 ||
    sample_size <= 0 ||
    confidence <= 0.0 ||
    confidence >= 1.0 {
    raise @core.GeometryError::DegenerateInput(
      "invalid adaptive RANSAC parameters",
    )
  }
  let mut success = 1.0
  for _ in 0.. 1.0 - confidence {
      probability *= failure
      answer += 1
    }
    answer
  }
}

///|
pub fn threshold_from_mad(
  residuals : ArrayView[Double],
  multiplier? : Double = 2.5,
) -> Double raise @core.GeometryError {
  if multiplier <= 0.0 {
    raise @core.GeometryError::DegenerateInput(
      "MAD multiplier must be positive",
    )
  }
  let scale = @core.robust_scale(residuals)
  let center = @core.median(residuals)
  center + multiplier * 1.4826 * scale
}

///|
pub fn score_residuals(
  residuals : ArrayView[Double],
  threshold~ : Double,
) -> RansacResult[Array[Double]] raise @core.GeometryError {
  if threshold <= 0.0 {
    raise @core.GeometryError::DegenerateInput(
      "residual threshold must be positive",
    )
  }
  let mask : Array[Bool] = []
  let mut count = 0
  let mut score = 0.0
  for residual in residuals {
    let ok = residual <= threshold
    mask.push(ok)
    if ok {
      count += 1
      score += residual
    }
  }
  {
    model: residuals.to_owned(),
    inliers: InlierMask::{ values: mask },
    inlier_count: count,
    score,
  }
}

///|
pub fn tukey_weight(residual~ : Double, cutoff~ : Double) -> Double {
  if cutoff <= 0.0 {
    0.0
  } else {
    let u = residual / cutoff
    if @core.abs(u) >= 1.0 {
      0.0
    } else {
      let v = 1.0 - u * u
      v * v
    }
  }
}

///|
pub fn huber_loss(residual~ : Double, delta? : Double = 1.0) -> Double {
  let a = @core.abs(residual)
  if a <= delta {
    0.5 * a * a
  } else {
    delta * (a - 0.5 * delta)
  }
}

///|
pub fn robust_cost(
  residuals : ArrayView[Double],
  delta? : Double = 1.0,
) -> Double {
  let mut total = 0.0
  for residual in residuals {
    total += huber_loss(residual~, delta~)
  }
  total
}

///|
pub fn consensus_ratio(mask : InlierMask) -> Double {
  if mask.length() == 0 {
    0.0
  } else {
    Double::from_int(mask.count()) / Double::from_int(mask.length())
  }
}

///|
pub fn deterministic_indices(
  length~ : Int,
  count~ : Int,
  seed~ : UInt,
) -> Array[Int] raise @core.GeometryError {
  if length <= 0 || count <= 0 || count > length {
    raise @core.GeometryError::DegenerateInput("sample dimensions are invalid")
  }
  let result : Array[Int] = []
  let mut state = seed
  for i in 0.. (@core.Point3, Double) raise @core.GeometryError {
  let p50 = @core.percentile(residuals, fraction=0.5)
  let p90 = @core.percentile(residuals, fraction=0.9)
  let p99 = @core.percentile(residuals, fraction=0.99)
  (@core.Point3::new(x=p50, y=p90, z=p99), @core.robust_scale(residuals))
}