///|
/// Decision-oriented summaries for uncertainty and scenario studies.
pub(all) struct RiskBand {
name : String
lower : Double
central : Double
upper : Double
probability : Double
severity : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct ScenarioMetric {
name : String
value : Double
weight : Double
feasible : Bool
} derive(Debug, Eq, ToJson)
///|
pub(all) struct SensitivityContribution {
parameter : String
elasticity : Double
absolute_effect : Double
rank : Int
direction : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct DecisionSummary {
target : String
central_value : Double
lower_bound : Double
upper_bound : Double
probability_of_success : Double
confidence : Double
decision : String
limiting_factor : String
} derive(Debug, Eq, ToJson)
///|
pub fn percentile(
values : ArrayView[Double],
probability : Double,
) -> Double raise @domain.ExtractionError {
if values.length() == 0 || probability < 0.0 || probability > 1.0 {
raise @domain.ExtractionError::InvalidArgument(
message="percentile input is invalid",
)
}
let ordered = values.to_owned()
ordered.sort()
let position = probability * (ordered.length() - 1).to_double()
let lower = position.to_int()
let upper = (lower + 1).min(ordered.length() - 1)
let fraction = position - lower.to_double()
ordered[lower] + fraction * (ordered[upper] - ordered[lower])
}
///|
pub fn risk_band(
name : String,
values : ArrayView[Double],
lower_probability : Double,
upper_probability : Double,
target : Double,
) -> RiskBand raise @domain.ExtractionError {
let lower = percentile(values, lower_probability)
let central = percentile(values, 0.5)
let upper = percentile(values, upper_probability)
let mut success = 0
for value in values {
if value >= target {
success += 1
}
}
let probability = success.to_double() / values.length().to_double()
{
name,
lower,
central,
upper,
probability,
severity: if probability >= 0.95 {
"low"
} else if probability >= 0.8 {
"moderate"
} else {
"high"
},
}
}
///|
pub fn weighted_scenario_mean(
scenarios : ArrayView[ScenarioMetric],
) -> Double raise @domain.ExtractionError {
if scenarios.length() == 0 {
raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
}
let mut total = 0.0
let mut weights = 0.0
for scenario in scenarios {
if scenario.weight < 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="scenario weight is negative",
)
}
total += scenario.value * scenario.weight
weights += scenario.weight
}
if weights == 0.0 {
raise @domain.ExtractionError::InvalidArgument(
message="scenario weights are zero",
)
}
total / weights
}
///|
pub fn feasible_probability(scenarios : ArrayView[ScenarioMetric]) -> Double {
if scenarios.length() == 0 {
return 0.0
}
let mut total = 0.0
let mut feasible = 0.0
for scenario in scenarios {
total += scenario.weight
if scenario.feasible {
feasible += scenario.weight
}
}
if total == 0.0 {
0.0
} else {
feasible / total
}
}
///|
pub fn scenario_value_range(
scenarios : ArrayView[ScenarioMetric],
) -> (Double, Double) {
if scenarios.length() == 0 {
return (0.0, 0.0)
}
let mut lower = scenarios[0].value
let mut upper = lower
for scenario in scenarios {
lower = lower.min(scenario.value)
upper = upper.max(scenario.value)
}
(lower, upper)
}
///|
pub fn sensitivity_contributions(
parameters : ArrayView[String],
elasticities : ArrayView[Double],
baseline : Double,
) -> Array[SensitivityContribution] {
let count = parameters.length().min(elasticities.length())
let result = []
for index in 0.. 0.0 {
"positive"
} else if elasticity < 0.0 {
"negative"
} else {
"neutral"
},
})
}
for first in 0.. result[first].absolute_effect {
rank += 1
}
}
result[first] = {
parameter: result[first].parameter,
elasticity: result[first].elasticity,
absolute_effect: result[first].absolute_effect,
rank,
direction: result[first].direction,
}
}
result
}
///|
pub fn limiting_parameter(
contributions : ArrayView[SensitivityContribution],
) -> String {
if contributions.length() == 0 {
return ""
}
let mut selected = contributions[0]
for contribution in contributions {
if contribution.absolute_effect > selected.absolute_effect {
selected = contribution
}
}
selected.parameter
}
///|
pub fn decide(
target : String,
values : ArrayView[Double],
target_value : Double,
minimum_probability : Double,
confidence : Double,
) -> DecisionSummary raise @domain.ExtractionError {
let band = risk_band(target, values, 0.05, 0.95, target_value)
let probability = band.probability
{
target,
central_value: band.central,
lower_bound: band.lower,
upper_bound: band.upper,
probability_of_success: probability,
confidence,
decision: if probability >= minimum_probability {
"accept"
} else {
"review"
},
limiting_factor: if band.severity == "high" {
"uncertainty"
} else {
"none"
},
}
}
///|
pub fn decision_is_robust(
summary : DecisionSummary,
minimum_probability : Double,
minimum_confidence : Double,
) -> Bool {
summary.probability_of_success >= minimum_probability &&
summary.confidence >= minimum_confidence &&
summary.decision == "accept"
}
///|
pub fn risk_band_markdown(bands : ArrayView[RiskBand]) -> String {
let lines = [
"| Metric | Lower | Central | Upper | Success probability | Severity |", "|---|---:|---:|---:|---:|---|",
]
for band in bands {
lines.push(
"| " +
band.name +
" | " +
band.lower.to_string() +
" | " +
band.central.to_string() +
" | " +
band.upper.to_string() +
" | " +
band.probability.to_string() +
" | " +
band.severity +
" |",
)
}
lines.join("\n")
}
///|
pub fn sensitivity_markdown(
contributions : ArrayView[SensitivityContribution],
) -> String {
let lines = [
"| Rank | Parameter | Elasticity | Absolute effect | Direction |", "|---:|---|---:|---:|---|",
]
for contribution in contributions {
lines.push(
"| " +
contribution.rank.to_string() +
" | " +
contribution.parameter +
" | " +
contribution.elasticity.to_string() +
" | " +
contribution.absolute_effect.to_string() +
" | " +
contribution.direction +
" |",
)
}
lines.join("\n")
}
///|
pub fn decision_markdown(summary : DecisionSummary) -> String {
"| Target | Central | Lower | Upper | Probability | Confidence | Decision |\n" +
"|---|---:|---:|---:|---:|---:|---|\n" +
"| " +
summary.target +
" | " +
summary.central_value.to_string() +
" | " +
summary.lower_bound.to_string() +
" | " +
summary.upper_bound.to_string() +
" | " +
summary.probability_of_success.to_string() +
" | " +
summary.confidence.to_string() +
" | " +
summary.decision +
" |"
}
///|
pub fn scenario_metrics_csv(scenarios : ArrayView[ScenarioMetric]) -> String {
let lines = ["name,value,weight,feasible"]
for scenario in scenarios {
lines.push(
scenario.name +
"," +
scenario.value.to_string() +
"," +
scenario.weight.to_string() +
"," +
scenario.feasible.to_string(),
)
}
lines.join("\n")
}
///|
pub fn bootstrap_mean(values : ArrayView[Double]) -> Double {
if values.length() == 0 {
return 0.0
}
let mut total = 0.0
for value in values {
total += value
}
total / values.length().to_double()
}
///|
pub fn bootstrap_standard_error(values : ArrayView[Double]) -> Double {
if values.length() < 2 {
return 0.0
}
let mean = bootstrap_mean(values)
let mut total = 0.0
for value in values {
let difference = value - mean
total += difference * difference
}
(total / (values.length() - 1).to_double()).sqrt() /
values.length().to_double().sqrt()
}
///|
pub fn uncertainty_width(summary : DecisionSummary) -> Double {
summary.upper_bound - summary.lower_bound
}
///|
pub fn uncertainty_width_relative(summary : DecisionSummary) -> Double {
if summary.central_value == 0.0 {
0.0
} else {
uncertainty_width(summary) / summary.central_value.abs()
}
}