///|
/// A bounded variable in a process design space.
pub(all) struct DesignVariable {
name : String
lower : Double
upper : Double
samples : Int
} derive(Debug, Eq, ToJson)
///|
pub fn DesignVariable::new(
name~ : String,
lower~ : Double,
upper~ : Double,
samples~ : Int,
) -> DesignVariable raise @domain.ExtractionError {
if name.trim().length() == 0 {
raise @domain.ExtractionError::InvalidArgument(
message="design variable name cannot be blank",
)
}
@domain.require_finite("design lower", lower)
@domain.require_finite("design upper", upper)
if upper < lower || samples < 1 {
raise @domain.ExtractionError::InvalidArgument(
message="design variable bounds or samples are invalid",
)
}
{ name, lower, upper, samples }
}
///|
pub fn DesignVariable::values(self : DesignVariable) -> Array[Double] {
if self.samples == 1 {
[self.lower]
} else {
[
for index in 0.. {
self.lower +
(self.upper - self.lower) *
index.to_double() /
(self.samples - 1).to_double()
}
]
}
}
///|
pub fn DesignVariable::midpoint(self : DesignVariable) -> Double {
(self.lower + self.upper) / 2.0
}
///|
pub fn DesignVariable::contains(self : DesignVariable, value : Double) -> Bool {
value >= self.lower && value <= self.upper
}
///|
pub(all) enum ConstraintKind {
AtLeast(target~ : Double)
AtMost(target~ : Double)
Equal(target~ : Double, tolerance~ : Double)
} derive(Debug, Eq, ToJson)
///|
pub(all) struct DesignConstraint {
name : String
kind : ConstraintKind
} derive(Debug, Eq, ToJson)
///|
pub fn DesignConstraint::new(
name~ : String,
kind~ : ConstraintKind,
) -> DesignConstraint raise @domain.ExtractionError {
if name.trim().length() == 0 {
raise @domain.ExtractionError::InvalidArgument(
message="constraint name cannot be blank",
)
}
match kind {
AtLeast(target~) => @domain.require_finite("constraint target", target)
AtMost(target~) => @domain.require_finite("constraint target", target)
Equal(target~, tolerance~) => {
@domain.require_finite("constraint target", target)
@domain.require_positive("constraint tolerance", tolerance)
}
}
{ name, kind }
}
///|
pub fn DesignConstraint::violation(
self : DesignConstraint,
value : Double,
) -> Double {
match self.kind {
AtLeast(target~) => (target - value).max(0.0)
AtMost(target~) => (value - target).max(0.0)
Equal(target~, tolerance~) =>
(value - target).abs().max(tolerance) - tolerance
}
}
///|
pub fn DesignConstraint::passes(
self : DesignConstraint,
value : Double,
) -> Bool {
self.violation(value) <= 0.0
}
///|
pub(all) struct DesignCandidate {
values : Array[Double]
objective : Double
constraint_values : Array[Double]
total_violation : Double
feasible : Bool
rank : Int
} derive(Debug, Eq, ToJson)
///|
pub fn DesignCandidate::new(
values~ : Array[Double],
objective~ : Double,
constraint_values~ : Array[Double],
constraints~ : ArrayView[DesignConstraint],
) -> DesignCandidate raise @domain.ExtractionError {
@domain.require_finite("design objective", objective)
if constraint_values.length() != constraints.length() {
raise @domain.ExtractionError::InvalidArgument(
message="constraint value and definition dimensions differ",
)
}
let mut violation = 0.0
for index, constraint in constraints {
@domain.require_finite("constraint value", constraint_values[index])
violation += constraint.violation(constraint_values[index])
}
{
values,
objective,
constraint_values,
total_violation: violation,
feasible: violation <= 0.0,
rank: 0,
}
}
///|
pub fn DesignCandidate::penalized_objective(
self : DesignCandidate,
penalty~ : Double,
) -> Double raise @domain.ExtractionError {
@domain.require_non_negative("constraint penalty", penalty)
self.objective + penalty * self.total_violation * self.total_violation
}
///|
pub fn DesignCandidate::dominates(
self : DesignCandidate,
other : DesignCandidate,
) -> Bool {
let self_feasible = self.feasible
let other_feasible = other.feasible
if self_feasible && !other_feasible {
true
} else if !self_feasible && other_feasible {
false
} else if !self_feasible && !other_feasible {
self.total_violation < other.total_violation
} else {
self.objective <= other.objective &&
(self.objective < other.objective || self.values != other.values)
}
}
///|
pub(all) struct DesignSpace {
variables : Array[DesignVariable]
constraints : Array[DesignConstraint]
} derive(Debug, Eq, ToJson)
///|
pub fn DesignSpace::new(
variables~ : ArrayView[DesignVariable],
constraints? : ArrayView[DesignConstraint] = [],
) -> DesignSpace raise @domain.ExtractionError {
if variables.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
{ variables: variables.to_owned(), constraints: constraints.to_owned() }
}
///|
pub fn DesignSpace::dimension(self : DesignSpace) -> Int {
self.variables.length()
}
///|
pub fn DesignSpace::variables(self : DesignSpace) -> Array[DesignVariable] {
self.variables.copy()
}
///|
pub fn DesignSpace::constraints(self : DesignSpace) -> Array[DesignConstraint] {
self.constraints.copy()
}
///|
fn enumerate_values(
variables : ArrayView[DesignVariable],
dimension : Int,
prefix : Array[Double],
output : Array[Array[Double]],
) -> Unit {
if dimension == variables.length() {
output.push(prefix)
} else {
for value in variables[dimension].values() {
let next = prefix.copy()
next.push(value)
enumerate_values(variables, dimension + 1, next, output)
}
}
}
///|
pub fn DesignSpace::grid(self : DesignSpace) -> Array[Array[Double]] {
let output : Array[Array[Double]] = []
enumerate_values(self.variables, 0, [], output)
output
}
///|
pub(all) struct OptimizationSummary {
candidates : Array[DesignCandidate]
feasible_count : Int
best : DesignCandidate?
pareto_front : Array[DesignCandidate]
evaluated : Int
} derive(Debug, Eq, ToJson)
///|
fn sorted_by_objective(
candidates : ArrayView[DesignCandidate],
) -> Array[DesignCandidate] {
let result : Array[DesignCandidate] = []
for candidate in candidates {
let mut position = result.length()
for index, other in result {
if candidate.objective < other.objective {
position = index
break
}
}
result.push(candidate)
let mut index = result.length() - 1
while index > position {
let value = result[index]
result[index] = result[index - 1]
result[index - 1] = value
index -= 1
}
}
result
}
///|
pub fn pareto_candidates(
candidates : ArrayView[DesignCandidate],
) -> Array[DesignCandidate] {
let result : Array[DesignCandidate] = []
for index, candidate in candidates {
let mut dominated = false
for other_index, other in candidates {
if index != other_index && other.dominates(candidate) {
dominated = true
}
}
if !dominated {
result.push(candidate)
}
}
result
}
///|
pub fn evaluate_grid(
space : DesignSpace,
evaluate : (ArrayView[Double]) -> (Double, Array[Double]) raise @domain.ExtractionError,
) -> OptimizationSummary raise @domain.ExtractionError {
let candidates : Array[DesignCandidate] = []
for values in space.grid() {
let (objective, constraint_values) = evaluate(values)
candidates.push(
DesignCandidate::new(
values~,
objective~,
constraint_values~,
constraints=space.constraints,
),
)
}
let sorted = sorted_by_objective(candidates)
let feasible = [ for candidate in sorted if candidate.feasible => candidate ]
let best = feasible.get(0)
{
candidates: sorted,
feasible_count: feasible.length(),
best,
pareto_front: pareto_candidates(sorted),
evaluated: sorted.length(),
}
}
///|
pub(all) struct CoordinateSearchResult {
values : Array[Double]
objective : Double
iterations : Int
evaluations : Int
converged : Bool
} derive(Debug, Eq, ToJson)
///|
/// Coordinate search for a bounded, derivative-free design objective.
pub fn coordinate_search(
variables : ArrayView[DesignVariable],
objective : (ArrayView[Double]) -> Double raise @domain.ExtractionError,
step_fraction? : Double = 0.25,
maximum_iterations? : Int = 80,
) -> CoordinateSearchResult raise @domain.ExtractionError {
if variables.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
@domain.require_positive("coordinate step fraction", step_fraction)
if step_fraction >= 1.0 {
raise @domain.ExtractionError::InvalidArgument(
message="coordinate step fraction must be below one",
)
}
let mut values = [ for variable in variables => variable.midpoint() ]
let mut objective_value = objective(values)
let mut step = step_fraction
let mut evaluations = 1
let mut converged = false
let mut iterations = 0
for iteration in 0.. DesignCandidate raise @domain.ExtractionError {
if candidates.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
let sorted = sorted_by_objective(candidates)
for candidate in sorted {
if candidate.feasible {
return candidate
}
}
let mut best = candidates[0]
for candidate in candidates {
if candidate.total_violation < best.total_violation {
best = candidate
}
}
best
}
///|
pub fn constraint_margin(
_candidate : DesignCandidate,
constraint : DesignConstraint,
value : Double,
) -> Double {
constraint.violation(value) * -1.0
}
///|
pub fn design_distance(
first : ArrayView[Double],
second : ArrayView[Double],
) -> Double raise @domain.ExtractionError {
if first.length() != second.length() {
raise @domain.ExtractionError::InvalidArgument(
message="design vectors have different dimensions",
)
}
let mut sum = 0.0
for index in 0.. Array[ConstraintAudit] raise @domain.ExtractionError {
if constraints.length() != candidate.constraint_values.length() {
raise @domain.ExtractionError::InvalidArgument(
message="audit dimensions differ",
)
}
[
for index, constraint in constraints => {
let violation = constraint.violation(candidate.constraint_values[index])
{
name: constraint.name,
value: candidate.constraint_values[index],
violation,
margin: -violation,
passed: violation <= 0.0,
}
}
]
}