///|
/// Format a nullable numeric result for stable human-readable reports.
pub fn format_optional(value : Double?, digits : Int) -> String {
match value {
None => "-"
Some(x) => format_double(x, digits~)
}
}
///|
/// Render a sensitivity report as Markdown.
pub fn sensitivity_to_markdown(report : SensitivityReport) -> String {
"| quantity | derivative |\n| --- | ---: |\n" +
"| conversion / volume | " +
format_double(report.conversion_wrt_volume) +
" |\n" +
"| conversion / rate constant | " +
format_double(report.conversion_wrt_rate_constant) +
" |\n" +
"| conversion / temperature | " +
format_double(report.conversion_wrt_temperature) +
" |\n" +
"| temperature / volume | " +
format_double(report.temperature_wrt_volume) +
" |\n" +
"| volume / target conversion | " +
format_double(report.volume_wrt_target_conversion) +
" |\n"
}
///|
/// Render an uncertainty summary as Markdown.
pub fn uncertainty_to_markdown(summary : UncertaintySummary) -> String {
"| statistic | value |\n| --- | ---: |\n" +
"| samples | " +
summary.samples.to_string() +
" |\n" +
"| minimum | " +
format_double(summary.minimum) +
" |\n" +
"| p05 | " +
format_double(summary.p05) +
" |\n" +
"| mean | " +
format_double(summary.mean) +
" |\n" +
"| p50 | " +
format_double(summary.p50) +
" |\n" +
"| p95 | " +
format_double(summary.p95) +
" |\n" +
"| maximum | " +
format_double(summary.maximum) +
" |\n" +
"| standard deviation | " +
format_double(summary.standard_deviation) +
" |\n"
}
///|
/// Render a cost estimate as Markdown.
pub fn cost_to_markdown(cost : CostEstimate) -> String {
"| component | annualized cost |\n| --- | ---: |\n" +
"| capital | " +
format_double(cost.capital) +
" |\n" +
"| utilities | " +
format_double(cost.utilities) +
" |\n" +
"| maintenance | " +
format_double(cost.maintenance) +
" |\n" +
"| total | " +
format_double(cost.total) +
" |\n"
}
///|
/// Render an envelope table with feasibility reasons.
pub fn envelope_to_markdown(points : ArrayView[EnvelopePoint]) -> String {
points.fold(
init="| volume | conversion | temperature | feasible | reason |\n| ---: | ---: | ---: | :---: | --- |\n",
fn(acc, point) {
acc +
"| " +
format_double(point.volume) +
" | " +
format_double(point.conversion) +
" | " +
format_double(point.temperature) +
" | " +
point.feasible.to_string() +
" | " +
point.reason +
" |\n"
},
)
}
///|
/// Render a controller trace as CSV.
pub fn controller_to_csv(trace : ArrayView[ControllerStep]) -> String {
let header = "index,output,integral,previous_error,saturated\n"
let mut index = 0
let mut output = header
for step in trace {
output = output +
index.to_string() +
"," +
format_double(step.output) +
"," +
format_double(step.state.integral) +
"," +
format_double(step.state.previous_error) +
"," +
step.saturated.to_string() +
"\n"
index = index + 1
}
output
}
///|
/// Render profile diagnostics as Markdown.
pub fn profile_summary(profile : ArrayView[ProfilePoint]) -> String {
if profile.length() == 0 {
"empty profile"
} else {
let first = profile[0]
let last = profile[profile.length() - 1]
"points=" +
profile.length().to_string() +
", conversion=" +
format_double(first.conversion) +
".." +
format_double(last.conversion) +
", temperature=" +
format_double(first.temperature) +
".." +
format_double(last.temperature) +
", exposure=" +
format_double(profile_rate_exposure(profile))
}
}
///|
/// Render a train summary as Markdown.
pub fn train_summary(result : TrainResult) -> String {
"stages=" +
result.stages.length().to_string() +
", total volume=" +
format_double(result.total_volume) +
", residence time=" +
format_double(result.total_residence_time) +
", final conversion=" +
format_double(result.final_conversion) +
", peak temperature=" +
format_double(train_peak_temperature(result))
}
///|
/// Render validation messages with a stable severity prefix.
pub fn validation_to_markdown(report : ValidationReport) -> String {
report.messages.fold(
init="| level | field | message |\n| --- | --- | --- |\n",
fn(acc, message) {
acc +
"| " +
message.level.to_string() +
" | " +
message.field +
" | " +
message.message +
" |\n"
},
)
}
///|
/// Render a benchmark result with an explicit status.
pub fn benchmark_result_to_markdown(result : BenchmarkResult) -> String {
let status = if result.passed { "PASS" } else { "FAIL" }
"| " +
result.id +
" | " +
status +
" | " +
format_double(result.actual) +
" | " +
format_double(result.absolute_error) +
" | " +
format_double(result.tolerance) +
" |\n"
}
///|
/// Render all benchmark results as a stable table.
pub fn benchmark_results_to_markdown(
results : ArrayView[BenchmarkResult],
) -> String {
results.fold(
init="| id | status | actual | error | tolerance |\n| --- | --- | ---: | ---: | ---: |\n",
fn(acc, result) { acc + benchmark_result_to_markdown(result) },
)
}
///|
/// Render an objective surface as CSV.
pub fn objective_surface_to_csv(scores : ArrayView[ObjectiveScore]) -> String {
scores.fold(init="volume,conversion,temperature,score,feasible\n", fn(
acc,
score,
) {
acc +
format_double(score.volume) +
"," +
format_double(score.conversion) +
"," +
format_double(score.temperature) +
"," +
format_double(score.score) +
"," +
score.feasible.to_string() +
"\n"
})
}
///|
/// Render a design point as a machine-readable CSV row.
pub fn design_point_to_csv_row(point : DesignPoint) -> String {
point.kind.to_string() +
"," +
point.thermal_mode.to_string() +
"," +
format_double(point.volume) +
"," +
format_double(point.residence_time) +
"," +
format_double(point.conversion) +
"," +
format_double(point.outlet_concentration) +
"," +
format_double(point.outlet_temperature) +
"," +
format_double(point.heat_removed) +
"," +
format_double(point.rate_at_outlet) +
"\n"
}
///|
/// Render a list of design points with a stable header.
pub fn design_points_to_csv(points : ArrayView[DesignPoint]) -> String {
points.fold(
init="kind,thermal_mode,volume,residence_time,conversion,outlet_concentration,outlet_temperature,heat_removed,rate_at_outlet\n",
fn(acc, point) { acc + design_point_to_csv_row(point) },
)
}