///|
/// A candidate with multiple engineering objectives.
pub(all) struct ObjectiveVector {
  name : String
  values : Array[Double]
  minimize : Array[Bool]
} derive(Debug, Eq, ToJson)

///|
pub fn ObjectiveVector::new(
  name~ : String,
  values~ : ArrayView[Double],
  minimize~ : ArrayView[Bool],
) -> ObjectiveVector raise @domain.ExtractionError {
  if name.trim().length() == 0 || values.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="objective vector is empty",
    )
  }
  if values.length() != minimize.length() {
    raise @domain.ExtractionError::InvalidArgument(
      message="objective directions do not match values",
    )
  }
  for value in values {
    @domain.require_finite("objective value", value)
  }
  { name, values: values.to_owned(), minimize: minimize.to_owned() }
}

///|
pub fn ObjectiveVector::dominates(
  self : ObjectiveVector,
  other : ObjectiveVector,
) -> Bool {
  if self.values.length() != other.values.length() {
    false
  } else {
    let mut strict = false
    let mut no_worse = true
    for index in 0.. b {
          no_worse = false
        }
        if a < b {
          strict = true
        }
      } else {
        if a < b {
          no_worse = false
        }
        if a > b {
          strict = true
        }
      }
    }
    no_worse && strict
  }
}

///|
pub(all) struct ObjectiveNormalization {
  minimum : Array[Double]
  maximum : Array[Double]
  normalized : Array[ObjectiveVector]
} derive(Debug, Eq, ToJson)

///|
pub fn normalize_objectives(
  vectors : ArrayView[ObjectiveVector],
) -> ObjectiveNormalization raise @domain.ExtractionError {
  if vectors.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let dimension = vectors[0].values.length()
  let minimum = Array::make(dimension, 1.0e300)
  let maximum = Array::make(dimension, -1.0e300)
  for vector in vectors {
    if vector.values.length() != dimension {
      raise @domain.ExtractionError::InvalidArgument(
        message="objective dimensions differ",
      )
    }
    for index, value in vector.values {
      minimum[index] = minimum[index].min(value)
      maximum[index] = maximum[index].max(value)
    }
  }
  let normalized = [
    for vector in vectors => {
      {
        name: vector.name,
        values: [
          for index, value in vector.values => {
            if maximum[index] <= minimum[index] {
              0.0
            } else {
              (value - minimum[index]) / (maximum[index] - minimum[index])
            }
          }
        ],
        minimize: vector.minimize,
      }
    }
  ]
  { minimum, maximum, normalized }
}

///|
pub fn pareto_layers(
  vectors : ArrayView[ObjectiveVector],
) -> Array[Array[ObjectiveVector]] {
  let remaining = vectors.to_owned()
  let layers : Array[Array[ObjectiveVector]] = []
  while remaining.length() > 0 {
    let front : Array[ObjectiveVector] = []
    for index, candidate in remaining {
      let mut dominated = false
      for other_index, other in remaining {
        if index != other_index && other.dominates(candidate) {
          dominated = true
        }
      }
      if !dominated {
        front.push(candidate)
      }
    }
    if front.length() == 0 {
      break
    }
    layers.push(front)
    let next : Array[ObjectiveVector] = []
    for candidate in remaining {
      let mut present = false
      for point in front {
        if point.name == candidate.name {
          present = true
        }
      }
      if !present {
        next.push(candidate)
      }
    }
    remaining.clear()
    for candidate in next {
      remaining.push(candidate)
    }
  }
  layers
}

///|
pub fn weighted_objective(
  vector : ObjectiveVector,
  weights : ArrayView[Double],
) -> Double raise @domain.ExtractionError {
  if vector.values.length() != weights.length() {
    raise @domain.ExtractionError::InvalidArgument(
      message="objective weights do not match values",
    )
  }
  let mut total = 0.0
  for index, value in vector.values {
    @domain.require_non_negative("objective weight", weights[index])
    total += if vector.minimize[index] {
      value * weights[index]
    } else {
      -value * weights[index]
    }
  }
  total
}

///|
pub(all) struct KneePoint {
  vector : ObjectiveVector
  distance : Double
  index : Int
} derive(Debug, Eq, ToJson)

///|
pub fn find_knee(
  vectors : ArrayView[ObjectiveVector],
) -> KneePoint raise @domain.ExtractionError {
  if vectors.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let normalized = normalize_objectives(vectors)
  let mut best = 0
  let mut best_distance = 1.0e300
  for index, vector in normalized.normalized {
    let mut distance = 0.0
    for value in vector.values {
      distance += (value - 0.5) * (value - 0.5)
    }
    if distance < best_distance {
      best = index
      best_distance = distance
    }
  }
  { vector: vectors[best], distance: best_distance.sqrt(), index: best }
}

///|
pub fn objective_spread(
  vectors : ArrayView[ObjectiveVector],
) -> Array[Double] raise @domain.ExtractionError {
  let normalized = normalize_objectives(vectors)
  let dimension = normalized.minimum.length()
  [
    for index in 0.. {
      normalized.maximum[index] - normalized.minimum[index]
    }
  ]
}

///|
pub fn diversity_score(
  vectors : ArrayView[ObjectiveVector],
) -> Double raise @domain.ExtractionError {
  if vectors.length() < 2 {
    return 0.0
  }
  let normalized = normalize_objectives(vectors).normalized
  let mut total = 0.0
  let mut pairs = 0
  for first in 0..