///|
pub(all) struct ScenarioRow {
index : Int
name : String
factors : Array[Double]
response : Double
weight : Double
feasible : Bool
} derive(Debug, Eq, ToJson)
///|
pub(all) struct ScenarioMatrix {
factor_names : Array[String]
rows : Array[ScenarioRow]
response_mean : Double
response_minimum : Double
response_maximum : Double
feasible_fraction : Double
} derive(Debug, Eq, ToJson)
///|
pub(all) struct ScenarioSummary {
factor : String
low_response : Double
high_response : Double
response_change : Double
elasticity : Double
limiting_level : Double
} derive(Debug, Eq, ToJson)
///|
pub fn scenario_row(
index : Int,
name : String,
factors : ArrayView[Double],
response : Double,
weight : Double,
feasible : Bool,
) -> ScenarioRow {
{ index, name, factors: factors.to_owned(), response, weight, feasible }
}
///|
pub fn make_scenario_matrix(
factor_names : ArrayView[String],
rows : ArrayView[ScenarioRow],
) -> ScenarioMatrix {
let mut total = 0.0
let mut weights = 0.0
let mut minimum = if rows.length() == 0 { 0.0 } else { rows[0].response }
let mut maximum = minimum
let mut feasible = 0.0
for row in rows {
total += row.response * row.weight
weights += row.weight
minimum = minimum.min(row.response)
maximum = maximum.max(row.response)
if row.feasible {
feasible += row.weight
}
}
{
factor_names: factor_names.to_owned(),
rows: rows.to_owned(),
response_mean: if weights == 0.0 {
0.0
} else {
total / weights
},
response_minimum: minimum,
response_maximum: maximum,
feasible_fraction: if weights == 0.0 {
0.0
} else {
feasible / weights
},
}
}
///|
pub fn scenario_matrix_factor_count(matrix : ScenarioMatrix) -> Int {
matrix.factor_names.length()
}
///|
pub fn scenario_matrix_row_count(matrix : ScenarioMatrix) -> Int {
matrix.rows.length()
}
///|
pub fn scenario_matrix_values(matrix : ScenarioMatrix) -> Array[Double] {
[
for row in matrix.rows => row.response
]
}
///|
pub fn scenario_matrix_feasible_rows(
matrix : ScenarioMatrix,
) -> Array[ScenarioRow] {
let result = []
for row in matrix.rows {
if row.feasible {
result.push(row)
}
}
result
}
///|
pub fn scenario_matrix_infeasible_rows(
matrix : ScenarioMatrix,
) -> Array[ScenarioRow] {
let result = []
for row in matrix.rows {
if !row.feasible {
result.push(row)
}
}
result
}
///|
pub fn scenario_matrix_response_range(matrix : ScenarioMatrix) -> Double {
matrix.response_maximum - matrix.response_minimum
}
///|
pub fn scenario_matrix_is_robust(
matrix : ScenarioMatrix,
minimum_fraction : Double,
) -> Bool {
matrix.feasible_fraction >= minimum_fraction
}
///|
pub fn scenario_matrix_worst_row(matrix : ScenarioMatrix) -> ScenarioRow? {
if matrix.rows.length() == 0 {
None
} else {
let mut selected = matrix.rows[0]
for row in matrix.rows {
if row.response < selected.response {
selected = row
}
}
Some(selected)
}
}
///|
pub fn scenario_matrix_best_row(matrix : ScenarioMatrix) -> ScenarioRow? {
if matrix.rows.length() == 0 {
None
} else {
let mut selected = matrix.rows[0]
for row in matrix.rows {
if row.response > selected.response {
selected = row
}
}
Some(selected)
}
}
///|
pub fn scenario_matrix_filter_factor(
matrix : ScenarioMatrix,
factor_index : Int,
lower : Double,
upper : Double,
) -> ScenarioMatrix raise @domain.ExtractionError {
if factor_index < 0 || factor_index >= matrix.factor_names.length() {
raise @domain.ExtractionError::OutOfRange(
name="factor index",
value=factor_index.to_double(),
lower=0.0,
upper=(matrix.factor_names.length() - 1).to_double(),
)
}
let selected = []
for row in matrix.rows {
if row.factors[factor_index] >= lower && row.factors[factor_index] <= upper {
selected.push(row)
}
}
make_scenario_matrix(matrix.factor_names, selected)
}
///|
pub fn scenario_matrix_filter_feasible(
matrix : ScenarioMatrix,
feasible : Bool,
) -> ScenarioMatrix {
let selected = []
for row in matrix.rows {
if row.feasible == feasible {
selected.push(row)
}
}
make_scenario_matrix(matrix.factor_names, selected)
}
///|
pub fn scenario_matrix_sort_by_response(
matrix : ScenarioMatrix,
ascending : Bool,
) -> ScenarioMatrix {
let rows = matrix.rows.copy()
for first in 0.. rows[selected].response
}
if should_select {
selected = second
}
}
let temporary = rows[first]
rows[first] = rows[selected]
rows[selected] = temporary
}
make_scenario_matrix(matrix.factor_names, rows)
}
///|
pub fn scenario_matrix_top(
matrix : ScenarioMatrix,
count : Int,
) -> Array[ScenarioRow] {
let sorted = scenario_matrix_sort_by_response(matrix, false)
let result = []
for index in 0.. Array[ScenarioRow] {
let sorted = scenario_matrix_sort_by_response(matrix, true)
let result = []
for index in 0.. Array[Double] raise @domain.ExtractionError {
if factor_index < 0 || factor_index >= matrix.factor_names.length() {
raise @domain.ExtractionError::OutOfRange(
name="factor index",
value=factor_index.to_double(),
lower=0.0,
upper=(matrix.factor_names.length() - 1).to_double(),
)
}
[
for row in matrix.rows => row.factors[factor_index]
]
}
///|
pub fn scenario_matrix_factor_mean(
matrix : ScenarioMatrix,
factor_index : Int,
) -> Double raise @domain.ExtractionError {
let values = scenario_matrix_factor_values(matrix, factor_index)
if values.length() == 0 {
0.0
} else {
values.fold(init=0.0, (sum, value) => sum + value) /
values.length().to_double()
}
}
///|
pub fn scenario_matrix_factor_range(
matrix : ScenarioMatrix,
factor_index : Int,
) -> (Double, Double) raise @domain.ExtractionError {
let values = scenario_matrix_factor_values(matrix, factor_index)
if values.length() == 0 {
(0.0, 0.0)
} else {
let mut lower = values[0]
let mut upper = lower
for value in values {
lower = lower.min(value)
upper = upper.max(value)
}
(lower, upper)
}
}
///|
pub fn scenario_matrix_response_correlation(
matrix : ScenarioMatrix,
factor_index : Int,
) -> Double raise @domain.ExtractionError {
let factors = scenario_matrix_factor_values(matrix, factor_index)
let responses = scenario_matrix_values(matrix)
if factors.length() != responses.length() || factors.length() == 0 {
raise @domain.ExtractionError::InsufficientData(
required=1,
actual=factors.length(),
)
}
let factor_mean = factors.fold(init=0.0, (sum, value) => sum + value) /
factors.length().to_double()
let response_mean = responses.fold(init=0.0, (sum, value) => sum + value) /
responses.length().to_double()
let mut covariance = 0.0
let mut factor_variance = 0.0
let mut response_variance = 0.0
for index in 0.. Array[ScenarioSummary] {
let summaries = []
for factor_index in 0.. high_level {
high_level = level
high_response = row.response
}
first = false
}
let change = high_response - low_response
summaries.push({
factor: matrix.factor_names[factor_index],
low_response,
high_response,
response_change: change,
elasticity: if high_level == low_level {
0.0
} else {
change / (high_level - low_level)
},
limiting_level: if low_response <= high_response {
low_level
} else {
high_level
},
})
}
summaries
}
///|
pub fn scenario_summary_markdown(
summaries : ArrayView[ScenarioSummary],
) -> String {
let lines = [
"| Factor | Low response | High response | Change | Elasticity | Limiting level |",
"|---|---:|---:|---:|---:|---:|",
]
for summary in summaries {
lines.push(
"| " +
summary.factor +
" | " +
summary.low_response.to_string() +
" | " +
summary.high_response.to_string() +
" | " +
summary.response_change.to_string() +
" | " +
summary.elasticity.to_string() +
" | " +
summary.limiting_level.to_string() +
" |",
)
}
lines.join("\n")
}
///|
pub fn scenario_matrix_csv(matrix : ScenarioMatrix) -> String {
let header = ["index", "name"]
let columns = header.copy()
columns.append(matrix.factor_names)
columns.append(["response", "weight", "feasible"])
let lines = [columns.join(",")]
for row in matrix.rows {
lines.push(
row.index.to_string() +
"," +
row.name +
"," +
row.factors.map(value => value.to_string()).join(",") +
"," +
row.response.to_string() +
"," +
row.weight.to_string() +
"," +
row.feasible.to_string(),
)
}
lines.join("\n")
}
///|
pub fn scenario_matrix_markdown(matrix : ScenarioMatrix) -> String {
let header = ["Index", "Name"]
header.append(matrix.factor_names)
header.push("Response")
header.push("Weight")
header.push("Feasible")
let lines = [
"| " + header.join(" | ") + " |",
"|" + Array::make(header.length(), "---").join("|") + "|",
]
for row in matrix.rows {
let cells = [row.index.to_string(), row.name]
cells.append(row.factors.map(value => value.to_string()))
cells.append([
row.response.to_string(),
row.weight.to_string(),
row.feasible.to_string(),
])
lines.push("| " + cells.join(" | ") + " |")
}
lines.join("\n")
}
///|
pub fn scenario_matrix_weighted_response(matrix : ScenarioMatrix) -> Double {
matrix.response_mean
}
///|
pub fn scenario_matrix_weighted_feasibility(matrix : ScenarioMatrix) -> Double {
matrix.feasible_fraction
}
///|
pub fn scenario_matrix_distance(
first : ScenarioMatrix,
second : ScenarioMatrix,
) -> Double {
let count = first.rows.length().min(second.rows.length())
let mut total = 0.0
for index in 0.. ScenarioMatrix {
let rows = first.rows.copy()
let offset = rows.length()
for row in second.rows {
rows.push({
index: offset + row.index,
name: row.name,
factors: row.factors,
response: row.response,
weight: row.weight,
feasible: row.feasible,
})
}
make_scenario_matrix(first.factor_names, rows)
}
///|
pub fn scenario_matrix_reweight(
matrix : ScenarioMatrix,
weights : ArrayView[Double],
) -> ScenarioMatrix {
let rows = []
for index in 0.. Double {
if matrix.rows.length() == 0 {
0.0
} else {
scenario_matrix_feasible_rows(matrix).length().to_double() /
matrix.rows.length().to_double()
}
}
///|
pub fn scenario_matrix_response_stddev(matrix : ScenarioMatrix) -> Double {
if matrix.rows.length() == 0 {
return 0.0
}
let mean = matrix.response_mean
let mut total = 0.0
for row in matrix.rows {
let difference = row.response - mean
total += difference * difference
}
(total / matrix.rows.length().to_double()).sqrt()
}
///|
pub fn scenario_matrix_confidence_width(matrix : ScenarioMatrix) -> Double {
2.0 * scenario_matrix_response_stddev(matrix)
}
///|
pub fn scenario_matrix_alerts(
matrix : ScenarioMatrix,
minimum_feasible_fraction : Double,
) -> Array[String] {
let alerts = []
if matrix.rows.length() == 0 {
alerts.push("matrix has no scenarios")
}
if matrix.feasible_fraction < minimum_feasible_fraction {
alerts.push("feasible fraction is below target")
}
if scenario_matrix_response_range(matrix) == 0.0 {
alerts.push("response has no observed variation")
}
alerts
}
///|
pub fn scenario_matrix_name_vector(matrix : ScenarioMatrix) -> Array[String] {
[
for row in matrix.rows => row.name
]
}
///|
pub fn scenario_matrix_response_vector(
matrix : ScenarioMatrix,
) -> Array[Double] {
[
for row in matrix.rows => row.response
]
}
///|
pub fn scenario_matrix_feasibility_vector(
matrix : ScenarioMatrix,
) -> Array[Bool] {
[
for row in matrix.rows => row.feasible
]
}