///|
/// 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..