///|
/// Robust design helpers for selecting operating points under scenarios.
pub(all) struct ScenarioEvaluation {
scenario : String
candidate : DesignCandidate
objective : Double
feasible : Bool
constraint_margin : Double
} derive(Debug, Eq, ToJson)
///|
pub(all) struct RobustCandidate {
candidate : DesignCandidate
worst_objective : Double
mean_objective : Double
best_objective : Double
worst_margin : Double
feasible_scenarios : Int
total_scenarios : Int
robust : Bool
} derive(Debug, Eq, ToJson)
///|
pub(all) struct RobustRanking {
candidates : Array[RobustCandidate]
selected_index : Int
risk_tolerance : Double
confidence : Double
} derive(Debug, Eq, ToJson)
///|
pub fn evaluate_candidate_scenarios(
candidate : DesignCandidate,
scenarios : ArrayView[String],
objective_values : ArrayView[Double],
margins : ArrayView[Double],
minimum_margin : Double,
) -> Array[ScenarioEvaluation] raise @domain.ExtractionError {
if scenarios.length() != objective_values.length() ||
scenarios.length() != margins.length() ||
scenarios.length() == 0 {
raise @domain.ExtractionError::InvalidArgument(
message="scenario evaluation arrays are invalid",
)
}
let result = []
for index in 0..= minimum_margin,
constraint_margin: margins[index],
})
}
result
}
///|
pub fn summarize_robust_candidate(
candidate : DesignCandidate,
evaluations : ArrayView[ScenarioEvaluation],
risk_tolerance : Double,
) -> RobustCandidate raise @domain.ExtractionError {
if evaluations.length() == 0 || risk_tolerance < 0.0 || risk_tolerance > 1.0 {
raise @domain.ExtractionError::InvalidArgument(
message="robust-candidate inputs are invalid",
)
}
let mut worst = evaluations[0].objective
let mut best = evaluations[0].objective
let mut total = 0.0
let mut margin = evaluations[0].constraint_margin
let mut feasible = 0
for evaluation in evaluations {
worst = worst.max(evaluation.objective)
best = best.min(evaluation.objective)
total += evaluation.objective
margin = margin.min(evaluation.constraint_margin)
if evaluation.feasible {
feasible += 1
}
}
let confidence = feasible.to_double() / evaluations.length().to_double()
{
candidate,
worst_objective: worst,
mean_objective: total / evaluations.length().to_double(),
best_objective: best,
worst_margin: margin,
feasible_scenarios: feasible,
total_scenarios: evaluations.length(),
robust: confidence >= 1.0 - risk_tolerance,
}
}
///|
pub fn robust_score(
candidate : RobustCandidate,
risk_weight : Double,
feasibility_weight : Double,
) -> Double {
let risk = candidate.worst_objective - candidate.best_objective
let feasibility = 1.0 -
candidate.feasible_scenarios.to_double() /
candidate.total_scenarios.to_double()
candidate.mean_objective +
risk_weight * risk +
feasibility_weight * feasibility
}
///|
pub fn rank_robust_candidates(
candidates : ArrayView[RobustCandidate],
risk_weight : Double,
feasibility_weight : Double,
risk_tolerance : Double,
) -> RobustRanking raise @domain.ExtractionError {
if candidates.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
let scores = [
for candidate in candidates => {
robust_score(candidate, risk_weight, feasibility_weight)
}
]
let mut selected = 0
let mut best_score = scores[0]
for index in 1.. Double {
if objective_values.length() == 0 {
return 0.0
}
let mut result = objective_values[0]
for value in objective_values {
result = result.max(value)
}
result
}
///|
pub fn best_case_objective(objective_values : ArrayView[Double]) -> Double {
if objective_values.length() == 0 {
return 0.0
}
let mut result = objective_values[0]
for value in objective_values {
result = result.min(value)
}
result
}
///|
pub fn scenario_regret(
objective_values : ArrayView[Double],
minimize : Bool,
) -> Array[Double] {
if objective_values.length() == 0 {
return []
}
let reference = if minimize {
best_case_objective(objective_values)
} else {
worst_case_objective(objective_values)
}
[
for value in objective_values => {
if minimize {
value - reference
} else {
reference - value
}
}
]
}
///|
pub fn regret_summary(
objective_values : ArrayView[Double],
minimize : Bool,
) -> (Double, Double, Double) {
let regrets = scenario_regret(objective_values, minimize)
if regrets.length() == 0 {
return (0.0, 0.0, 0.0)
}
let mut total = 0.0
let mut maximum = 0.0
for regret in regrets {
total += regret
maximum = maximum.max(regret)
}
(total / regrets.length().to_double(), maximum, regrets[regrets.length() - 1])
}
///|
pub fn chance_constraint(
values : ArrayView[Double],
limit : Double,
probability : Double,
lower_is_better : Bool,
) -> Bool raise @domain.ExtractionError {
if values.length() == 0 || probability < 0.0 || probability > 1.0 {
raise @domain.ExtractionError::InvalidArgument(
message="chance constraint inputs are invalid",
)
}
let mut satisfied = 0
for value in values {
if (if lower_is_better { value <= limit } else { value >= limit }) {
satisfied += 1
}
}
satisfied.to_double() / values.length().to_double() >= probability
}
///|
pub fn safety_factor(
nominal : Double,
limit : Double,
lower_is_better : Bool,
) -> Double {
if nominal == 0.0 {
return 0.0
}
if lower_is_better {
limit / nominal
} else {
nominal / limit
}
}
///|
pub fn robustness_index(candidate : RobustCandidate) -> Double {
let feasibility = candidate.feasible_scenarios.to_double() /
candidate.total_scenarios.to_double()
let margin = candidate.worst_margin.max(0.0)
feasibility * (1.0 + margin)
}
///|
pub fn robust_candidates(
candidates : ArrayView[RobustCandidate],
) -> Array[RobustCandidate] {
let selected = []
for candidate in candidates {
if candidate.robust {
selected.push(candidate)
}
}
selected
}
///|
pub fn robust_design_markdown(ranking : RobustRanking) -> String {
let lines = [
"| Rank | Objective mean | Worst | Best | Worst margin | Feasible scenarios | Robust |",
"|---:|---:|---:|---:|---:|---:|---|",
]
let indexes = [ for index in 0.. index ]
for index in indexes {
let candidate = ranking.candidates[index]
lines.push(
"| " +
index.to_string() +
" | " +
candidate.mean_objective.to_string() +
" | " +
candidate.worst_objective.to_string() +
" | " +
candidate.best_objective.to_string() +
" | " +
candidate.worst_margin.to_string() +
" | " +
candidate.feasible_scenarios.to_string() +
"/" +
candidate.total_scenarios.to_string() +
" | " +
candidate.robust.to_string() +
" |",
)
}
lines.join("\n")
}
///|
pub fn robust_design_csv(ranking : RobustRanking) -> String {
let lines = [
"index,mean_objective,worst_objective,best_objective,worst_margin,feasible,total,robust",
]
for index in 0.. Double raise @domain.ExtractionError {
if values.length() != weights.length() || values.length() == 0 {
raise @domain.ExtractionError::InvalidArgument(
message="weighted mean arrays are invalid",
)
}
let mut total = 0.0
let mut weight_total = 0.0
for index in 0.. Double raise @domain.ExtractionError {
if values.length() != weights.length() ||
values.length() == 0 ||
probability < 0.0 ||
probability > 1.0 {
raise @domain.ExtractionError::InvalidArgument(
message="weighted quantile inputs are invalid",
)
}
let pairs = []
for index in 0.. {
if first.0 < second.0 {
-1
} else if first.0 > second.0 {
1
} else {
0
}
})
let total = pairs.fold(init=0.0, (sum, pair) => sum + pair.1)
let target = total * probability
let mut cumulative = 0.0
for pair in pairs {
cumulative += pair.1
if cumulative >= target {
return pair.0
}
}
pairs[pairs.length() - 1].0
}