use std::io::{self, Write};
use henad_core::explore::outcome::RunOutcome;
use henad_core::explore::plan::Config;
use henad_core::explore::spec::ActionSpec;
use henad_core::explore::value::format_value;
use henad_core::export::csv::{escape_field, fmt_f64};
use henad_core::params::{ParamDescriptor, ParamKind};
pub const ID_COLUMNS: [&str; 6] = ["run_id", "config_id", "block", "rep", "seed", "run_key"];
pub const OUTCOME_COLUMNS: [&str; 7] = [
"status",
"stop_reason",
"ticks",
"population",
"build_ms",
"wall_ms",
"steps_per_s",
];
pub const TIMING_COLUMNS: [&str; 3] = ["build_ms", "wall_ms", "steps_per_s"];
pub const NOTE_COLUMN: &str = "note";
pub(crate) fn column_names(params: &[ParamDescriptor], actions: &[ActionSpec], reducers: &[String]) -> Vec<String> {
let mut names: Vec<String> = ID_COLUMNS.iter().map(|&column| column.to_owned()).collect();
names.extend(params.iter().map(|param| param.id.to_owned()));
names.extend(actions.iter().map(ActionSpec::column_name));
names.extend(OUTCOME_COLUMNS.iter().map(|&column| column.to_owned()));
names.extend(reducers.iter().cloned());
names.push(NOTE_COLUMN.to_owned());
names
}
pub(crate) fn header_line(names: &[String]) -> String {
let mut line = names
.iter()
.map(|name| escape_field(name))
.collect::<Vec<_>>()
.join(",");
line.push('\n');
line
}
#[derive(Debug)]
pub struct RunsWriter<W: Write> {
dest: W,
kinds: Vec<ParamKind>,
}
impl<W: Write> RunsWriter<W> {
pub fn new(
mut dest: W,
params: &[ParamDescriptor],
actions: &[ActionSpec],
reducers: &[String],
) -> io::Result<Self> {
dest.write_all(header_line(&column_names(params, actions, reducers)).as_bytes())?;
Ok(Self::appending(dest, params))
}
pub fn appending(dest: W, params: &[ParamDescriptor]) -> Self {
Self {
dest,
kinds: params.iter().map(|param| param.kind.clone()).collect(),
}
}
pub fn write_run(&mut self, outcome: &RunOutcome, config: &Config) -> io::Result<()> {
let run = &outcome.run;
let mut row = format!(
"{},{},{},{},{},{:016x}",
run.run_id, run.config_id, config.block, run.rep, run.seed, outcome.run_key
);
for (kind, value) in self.kinds.iter().zip(&config.params) {
row.push(',');
row.push_str(&escape_field(&format_value(kind, value)));
}
for tick in &config.action_ticks {
row.push_str(&format!(",{tick}"));
}
let timings = [outcome.build_ms, outcome.wall_ms, outcome.steps_per_s()].map(format_timing);
row.push_str(&format!(
",{},{},{},{},{},{},{}",
outcome.status.as_str(),
outcome.stop_reason.as_str(),
outcome.ticks,
outcome.population,
timings[0],
timings[1],
timings[2],
));
for value in &outcome.reducers {
row.push(',');
row.push_str(&value.map_or_else(String::new, fmt_f64));
}
row.push(',');
row.push_str(&escape_field(outcome.note.as_deref().unwrap_or_default()));
writeln!(self.dest, "{row}")
}
pub fn flush(&mut self) -> io::Result<()> {
self.dest.flush()
}
pub fn into_inner(mut self) -> io::Result<W> {
self.dest.flush()?;
Ok(self.dest)
}
}
fn format_timing(value: f64) -> String {
fmt_f64((value * 1000.0).round() / 1000.0)
}
#[cfg(test)]
mod tests {
use henad_core::explore::measure::SeriesBuffer;
use henad_core::explore::outcome::{PlannedRun, RunOutcome, RunStatus, StopReason};
use henad_core::explore::plan::Config;
use henad_core::explore::spec::ActionSpec;
use henad_core::helpers::{choice_param, f32_param};
use henad_core::params::ParamValue;
use super::{RunsWriter, format_timing};
const NETWORKS: &[&str] = &["Random", "Geometric"];
#[test]
fn a_row_holds_every_column_in_header_order() {
let params = [
f32_param("spread, chance", "Spread chance", 0.2, 0.0, 1.0, None),
choice_param("network", "Network", NETWORKS, 0),
];
let reducers = ["Infected:max".to_owned(), "Speed.[0, 1):min".to_owned()];
let actions = [ActionSpec {
name: "second, wave".to_owned(),
..ActionSpec::new("seed_outbreak", 40)
}];
let mut writer = RunsWriter::new(Vec::new(), ¶ms, &actions, &reducers).expect("a vector takes every write");
let outcome = RunOutcome {
run: PlannedRun {
run_id: 7,
config_id: 2,
rep: 1,
seed: 99,
},
run_key: 0xab,
status: RunStatus::Panicked,
stop_reason: StopReason::Fault,
ticks: 40,
population: 256,
build_ms: 1.23456,
wall_ms: 8.0,
reducers: vec![Some(12.5), None],
series: SeriesBuffer::new(2),
note: Some("while stepping, \"x\" panicked".to_owned()),
};
let config = Config {
block: 1,
params: vec![ParamValue::F32(0.1), ParamValue::Choice(1)],
action_ticks: vec![25],
};
writer.write_run(&outcome, &config).expect("a vector takes every write");
let text = String::from_utf8(writer.into_inner().expect("a vector flushes")).expect("the rows are UTF-8");
let lines: Vec<&str> = text.lines().collect();
assert_eq!(
lines[0],
"run_id,config_id,block,rep,seed,run_key,\"spread, chance\",network,\"action.second, wave\",status,\
stop_reason,ticks,population,build_ms,wall_ms,steps_per_s,Infected:max,\"Speed.[0, 1):min\",note"
);
assert_eq!(
lines[1],
"7,2,1,1,99,00000000000000ab,0.1,Geometric,25,panicked,fault,40,256,1.235,8,5000,12.5,,\
\"while stepping, \"\"x\"\" panicked\""
);
}
#[test]
fn a_timing_keeps_three_decimal_places() {
assert_eq!(format_timing(0.1 + 0.2), "0.3");
assert_eq!(format_timing(2.0), "2");
assert_eq!(format_timing(f64::INFINITY), "");
assert_eq!(format_timing(f64::NAN), "");
}
}