///|
pub struct ScenarioPoint {
name : String
input : Float
output : Float
score : Float
status : ResultStatus
note : String
} derive(Debug, Eq)
///|
pub fn scenario_point(
name : String,
input : Float,
output : Float,
score : Float,
status : ResultStatus,
note : String,
) -> ScenarioPoint {
{ name, input, output, score, status, note }
}
///|
pub fn ScenarioPoint::efficiency(self : ScenarioPoint) -> Float {
if self.input == 0.0 {
0.0
} else {
self.output / self.input
}
}
///|
pub fn ScenarioPoint::is_acceptable(
self : ScenarioPoint,
minimum_score : Float,
) -> Bool {
self.score >= minimum_score && self.status is Normal
}
///|
pub struct ScenarioSeries {
name : String
points : Array[ScenarioPoint]
input_unit : String
output_unit : String
} derive(Debug, Eq)
///|
pub fn scenario_series(
name : String,
points : Array[ScenarioPoint],
input_unit : String,
output_unit : String,
) -> ScenarioSeries {
{ name, points, input_unit, output_unit }
}
///|
pub fn ScenarioSeries::count(self : ScenarioSeries) -> Int {
self.points.length()
}
///|
pub fn ScenarioSeries::inputs(self : ScenarioSeries) -> Array[Float] {
self.points.map(fn(item) { item.input })
}
///|
pub fn ScenarioSeries::outputs(self : ScenarioSeries) -> Array[Float] {
self.points.map(fn(item) { item.output })
}
///|
pub fn ScenarioSeries::scores(self : ScenarioSeries) -> Array[Float] {
self.points.map(fn(item) { item.score })
}
///|
pub fn ScenarioSeries::summary(self : ScenarioSeries) -> Statistics {
summarize(self.scores())
}
///|
pub fn ScenarioSeries::best(self : ScenarioSeries) -> ScenarioPoint? {
if self.points.length() == 0 {
None
} else {
let mut result = self.points[0]
for point in self.points[1:] {
if point.score > result.score {
result = point
}
}
Some(result)
}
}
///|
pub fn ScenarioSeries::worst(self : ScenarioSeries) -> ScenarioPoint? {
if self.points.length() == 0 {
None
} else {
let mut result = self.points[0]
for point in self.points[1:] {
if point.score < result.score {
result = point
}
}
Some(result)
}
}
///|
pub fn ScenarioSeries::acceptable(
self : ScenarioSeries,
minimum_score : Float,
) -> Array[ScenarioPoint] {
let result : Array[ScenarioPoint] = []
for point in self.points {
if point.is_acceptable(minimum_score) {
result.push(point)
}
}
result
}
///|
pub fn ScenarioSeries::flagged(self : ScenarioSeries) -> Array[ScenarioPoint] {
let result : Array[ScenarioPoint] = []
for point in self.points {
if point.status != Normal {
result.push(point)
}
}
result
}
///|
pub fn ScenarioSeries::at(self : ScenarioSeries, index : Int) -> ScenarioPoint? {
self.points.get(index)
}
///|
pub fn ScenarioSeries::with_point(
self : ScenarioSeries,
point : ScenarioPoint,
) -> ScenarioSeries {
let points = self.points.copy()
points.push(point)
{ ..self, points, }
}
///|
pub fn ScenarioSeries::to_table(self : ScenarioSeries) -> ReportTable {
let rows : Array[Array[String]] = []
for point in self.points {
rows.push([
point.name,
"{point.input}",
"{point.output}",
"{point.score}",
point.note,
])
}
table(["scenario", self.input_unit, self.output_unit, "score", "note"], rows)
}
///|
pub struct SweepConfig {
start : Float
stop : Float
steps : Int
label : String
} derive(Debug, Eq)
///|
pub fn sweep_config(
start : Float,
stop : Float,
steps : Int,
label : String,
) -> SweepConfig {
{ start, stop, steps, label }
}
///|
pub fn SweepConfig::is_valid(self : SweepConfig) -> Bool {
self.steps >= 2 && self.stop >= self.start
}
///|
pub fn SweepConfig::values(self : SweepConfig) -> Array[Float] {
let result : Array[Float] = []
if !self.is_valid() {
return result
}
let delta = (self.stop - self.start) / Float::from_int(self.steps - 1)
for index in 0.. Float,
) -> ScenarioSeries {
let points : Array[ScenarioPoint] = []
for _, value in config.values() {
points.push(
scenario_point(
config.label,
value,
evaluate(value),
evaluate(value),
Normal,
"deterministic sweep",
),
)
}
scenario_series(config.label, points, "input", "output")
}
///|
pub fn ScenarioSeries::normalized(self : ScenarioSeries) -> ScenarioSeries {
let scores = self.scores()
let summary = summarize(scores)
let points : Array[ScenarioPoint] = []
for point in self.points {
let normalized : Float = if summary.standard_deviation == 0.0 {
0.0
} else {
(point.score - summary.mean) / summary.standard_deviation
}
points.push({ ..point, score: normalized })
}
{ ..self, points, }
}
///|
pub fn ScenarioSeries::merge(
self : ScenarioSeries,
other : ScenarioSeries,
) -> ScenarioSeries {
let points = self.points.copy()
for point in other.points {
points.push(point)
}
{
name: self.name + "+" + other.name,
points,
input_unit: self.input_unit,
output_unit: self.output_unit,
}
}
///|
pub fn ScenarioSeries::quality_gate(
self : ScenarioSeries,
minimum_score : Float,
maximum_flagged : Int,
) -> Bool {
self.acceptable(minimum_score).length() > 0 &&
self.flagged().length() <= maximum_flagged
}
///|
pub fn ScenarioSeries::to_markdown(self : ScenarioSeries) -> String {
self.to_table().to_markdown()
}
///|
pub fn ScenarioSeries::to_csv(self : ScenarioSeries) -> String {
self.to_table().to_csv()
}
///|
pub fn ScenarioSeries::report_section(self : ScenarioSeries) -> ReportSection {
{ title: self.name, kind: Method, body: self.to_markdown() }
}
///|
pub fn sensitivity(
base : Float,
delta : Float,
response_base : Float,
response_delta : Float,
) -> Float {
if delta == 0.0 || base == 0.0 {
0.0
} else {
(response_delta - response_base) / delta * base / response_base
}
}
///|
pub fn elasticity(
input_before : Float,
input_after : Float,
output_before : Float,
output_after : Float,
) -> Float {
if input_before == 0.0 || output_before == 0.0 {
0.0
} else {
percent_change(output_before, output_after) /
percent_change(input_before, input_after)
}
}
///|
pub fn scenario_distance(left : ScenarioPoint, right : ScenarioPoint) -> Float {
((left.input - right.input) * (left.input - right.input) +
(left.output - right.output) * (left.output - right.output)).sqrt()
}
///|
pub fn nearest_scenario(
series : ScenarioSeries,
input : Float,
) -> ScenarioPoint? {
if series.points.length() == 0 {
None
} else {
let mut result = series.points[0]
for point in series.points[1:] {
if (point.input - input).abs() < (result.input - input).abs() {
result = point
}
}
Some(result)
}
}
///|
pub fn interpolate_scenario(series : ScenarioSeries, input : Float) -> Float? {
match nearest_scenario(series, input) {
Some(point) => Some(point.output)
None => None
}
}
///|
pub fn scenario_status(
score : Float,
lower : Float,
upper : Float,
) -> ResultStatus {
if score < lower {
Flagged
} else if score > upper {
Estimated
} else {
Normal
}
}
///|
pub fn classify_series(
series : ScenarioSeries,
lower : Float,
upper : Float,
) -> ScenarioSeries {
let points : Array[ScenarioPoint] = []
for point in series.points {
points.push({ ..point, status: scenario_status(point.score, lower, upper) })
}
{ ..series, points, }
}