///|
pub struct ReportTable {
headers : Array[String]
rows : Array[Array[String]]
} derive(Debug)
///|
pub fn ReportTable::new(headers : Array[String]) -> ReportTable {
{ headers, rows: [] }
}
///|
pub fn ReportTable::add_row(self : ReportTable, row : Array[String]) -> Unit {
self.rows.push(row)
}
///|
pub fn ReportTable::row_count(self : ReportTable) -> Int {
self.rows.length()
}
///|
pub fn ReportTable::column_count(self : ReportTable) -> Int {
self.headers.length()
}
///|
pub fn ReportTable::valid_shape(self : ReportTable) -> Bool {
let mut valid = true
for row in self.rows {
if row.length() != self.headers.length() {
valid = false
}
}
valid
}
///|
pub fn ReportTable::to_csv(self : ReportTable) -> String {
let mut output = self.headers.join(",") + "\n"
for row in self.rows {
output = output + row.join(",") + "\n"
}
output
}
///|
pub fn ReportTable::to_markdown(self : ReportTable) -> String {
let mut output = "|" + self.headers.join("|") + "|\n|"
let separators = Array::make(self.headers.length(), "---")
output = output + separators.join("|") + "|\n"
for row in self.rows {
output = output + "|" + row.join("|") + "|\n"
}
output
}
///|
pub fn report_table_from_result(result : BenchmarkResult) -> ReportTable {
let table = ReportTable::new(["episode", "reward", "steps", "solved"])
for i in 0.. ReportTable {
let rewards = result.reward_stats()
let steps = result.step_stats()
let table = ReportTable::new(["metric", "value"])
table.add_row(["episodes", "\{result.episodes}"])
table.add_row(["solve_rate", "\{result.solve_rate()}"])
table.add_row(["reward_mean", "\{rewards.average()}"])
table.add_row(["reward_variance", "\{rewards.variance()}"])
table.add_row(["steps_mean", "\{steps.average()}"])
table.add_row(["tail_reward", "\{result.tail_average(20)}"])
table
}
///|
pub fn compare_results_table(results : Array[BenchmarkResult]) -> ReportTable {
let table = ReportTable::new([
"benchmark", "episodes", "solve_rate", "reward_mean", "steps_mean",
])
for result in results {
table.add_row([
result.name,
"\{result.episodes}",
"\{result.solve_rate()}",
"\{result.reward_stats().average()}",
"\{result.step_stats().average()}",
])
}
table
}
///|
pub fn run_standard_reports(config : EvaluationConfig) -> String {
let grid = run_gridworld_benchmark(config.episodes, config.seed).result
let random = run_random_walk_benchmark(config).result
let bandit = run_bandit_benchmark(config).result
compare_results_table([grid, random, bandit]).to_markdown()
}
///|
pub fn report_header(title : String, seed : Int) -> String {
"# \{title}\n\nseed: \{seed}\n\n"
}
///|
pub fn report_footer() -> String {
"\nGenerated by MoonRLLab deterministic benchmark tooling.\n"
}
///|
pub fn full_benchmark_report(config : EvaluationConfig) -> String {
report_header("MoonRLLab benchmark report", config.seed) +
run_standard_reports(config) +
report_footer()
}
///|
pub fn csv_escape(value : String) -> String {
if value.contains(",") || value.contains("\n") {
"\"" + value + "\""
} else {
value
}
}
///|
pub fn join_lines(lines : Array[String]) -> String {
lines.join("\n")
}
///|
pub fn percentile(values : Array[Double], ratio : Double) -> Double {
if values.length() == 0 {
0.0
} else {
let sorted = values.copy()
for i in 0.. String {
let stats = RunningStats::new()
for value in values {
stats.push(value)
}
let margin = 1.96 * stats.standard_error()
"mean=\{stats.average()},lower=\{stats.average() - margin},upper=\{stats.average() + margin}"
}
///|
pub fn report_quality(result : BenchmarkResult) -> ScorecardSet {
let scores = ScorecardSet::new()
scores.add(
Scorecard::new(
"has_episodes",
1.0,
if result.episodes > 0 {
1.0
} else {
0.0
},
0.0,
),
)
scores.add(
Scorecard::new(
"bounded_solve_rate",
1.0,
if result.solve_rate() >= 0.0 && result.solve_rate() <= 1.0 {
1.0
} else {
0.0
},
0.0,
),
)
scores.add(
Scorecard::new(
"csv_rows",
result.episodes.to_double() + 1.0,
result.to_csv().split("\n").length().to_double(),
0.0,
),
)
scores
}
///|
pub fn result_digest(result : BenchmarkResult) -> String {
let stats = result.reward_stats()
"\{result.name}|\{result.seed}|\{result.episodes}|\{stats.average()}|\{result.solve_rate()}"
}
///|
pub fn merge_reward_series(
left : Array[Double],
right : Array[Double],
) -> Array[Double] {
let result = left.copy()
for value in right {
result.push(value)
}
result
}
///|
pub fn moving_average(values : Array[Double], window : Int) -> Array[Double] {
let result = Array::make(values.length(), 0.0)
let size = if window < 1 { 1 } else { window }
for i in 0.. size { i + 1 - size } else { 0 }
let mut total = 0.0
let mut count = 0
for j in start..<(i + 1) {
total = total + values[j]
count = count + 1
}
result[i] = total / count.to_double()
}
result
}
///|
pub fn trend(values : Array[Double]) -> Double {
if values.length() < 2 {
0.0
} else {
values[values.length() - 1] - values[0]
}
}
///|
pub fn quality_text(result : BenchmarkResult) -> String {
let quality = report_quality(result)
"\{result_digest(result)}\n\{quality.to_text()}"
}
///|
pub fn report_sections(config : EvaluationConfig) -> Array[String] {
let result = run_random_walk_benchmark(config).result
[
report_header("Experiment", config.seed),
report_table_from_stats(result).to_markdown(),
quality_text(result),
report_footer(),
]
}
///|
pub fn report_sections_text(config : EvaluationConfig) -> String {
join_lines(report_sections(config))
}
///|
pub fn reward_floor(values : Array[Double]) -> Double {
if values.length() == 0 {
0.0
} else {
let mut result = values[0]
for value in values {
if value < result {
result = value
}
}
result
}
}
///|
pub fn reward_ceiling(values : Array[Double]) -> Double {
if values.length() == 0 {
0.0
} else {
let mut result = values[0]
for value in values {
if value > result {
result = value
}
}
result
}
}
///|
pub fn solved_prefix(values : Array[Bool]) -> Int {
let mut count = 0
for value in values {
if value {
count = count + 1
} else {
break
}
}
count
}
///|
pub fn result_range(result : BenchmarkResult) -> String {
let stats = result.reward_stats()
"min=\{stats.minimum},max=\{stats.maximum},span=\{stats.maximum - stats.minimum}"
}
///|
pub fn result_health(result : BenchmarkResult) -> String {
let valid = result.episodes >= 0 &&
result.rewards.length() == result.episodes &&
result.steps.length() == result.episodes
"valid=\{valid},\{result_range(result)}"
}
///|
pub fn markdown_escape(value : String) -> String {
value.replace(old="|", new="\\|").replace(old="\n", new=" ")
}
///|
pub fn table_preview(table : ReportTable, limit : Int) -> String {
let mut output = table.headers.join("|") + "\n"
let size = if limit < 0 {
0
} else if limit > table.rows.length() {
table.rows.length()
} else {
limit
}
for i in 0.. Bool {
table.row_count() == 0
}
///|
pub fn table_last_row(table : ReportTable) -> Array[String]? {
if table.rows.length() == 0 {
None
} else {
Some(table.rows[table.rows.length() - 1])
}
}
///|
pub fn result_is_nontrivial(result : BenchmarkResult) -> Bool {
result.episodes > 0 && result.reward_stats().count > 0
}
///|
pub fn result_has_learning_signal(result : BenchmarkResult) -> Bool {
trend(result.rewards) != 0.0 || result.solve_rate() > 0.0
}
///|
pub fn compact_result_line(result : BenchmarkResult) -> String {
"\{result.name}:\{result.episodes}:\{result.solve_rate()}:\{result.tail_average(10)}"
}
///|
pub fn compact_results(results : Array[BenchmarkResult]) -> String {
let lines = []
for result in results {
lines.push(compact_result_line(result))
}
join_lines(lines)
}
///|
pub fn report_version() -> String {
"MoonRLLab-report-v1"
}
///|
pub fn result_episode_ids(result : BenchmarkResult) -> Array[Int] {
let ids = Array::make(result.episodes, 0)
for i in 0.. Bool {
result_episode_ids(result).length() == result.episodes
}
///|
pub fn report_contract() -> String {
"contract=csv,markdown,summary,confidence,health"
}