1use std::io::{self, Write};
8
9use henad_core::explore::outcome::RunOutcome;
10use henad_core::explore::plan::Config;
11use henad_core::explore::spec::ActionSpec;
12use henad_core::explore::value::format_value;
13use henad_core::export::csv::{escape_field, fmt_f64};
14use henad_core::params::{ParamDescriptor, ParamKind};
15
16pub const ID_COLUMNS: [&str; 6] = ["run_id", "config_id", "block", "rep", "seed", "run_key"];
18
19pub const OUTCOME_COLUMNS: [&str; 7] = [
21 "status",
22 "stop_reason",
23 "ticks",
24 "population",
25 "build_ms",
26 "wall_ms",
27 "steps_per_s",
28];
29
30pub const TIMING_COLUMNS: [&str; 3] = ["build_ms", "wall_ms", "steps_per_s"];
32
33pub const NOTE_COLUMN: &str = "note";
35
36pub(crate) fn column_names(params: &[ParamDescriptor], actions: &[ActionSpec], reducers: &[String]) -> Vec<String> {
39 let mut names: Vec<String> = ID_COLUMNS.iter().map(|&column| column.to_owned()).collect();
40 names.extend(params.iter().map(|param| param.id.to_owned()));
41 names.extend(actions.iter().map(ActionSpec::column_name));
42 names.extend(OUTCOME_COLUMNS.iter().map(|&column| column.to_owned()));
43 names.extend(reducers.iter().cloned());
44 names.push(NOTE_COLUMN.to_owned());
45 names
46}
47
48pub(crate) fn header_line(names: &[String]) -> String {
50 let mut line = names
51 .iter()
52 .map(|name| escape_field(name))
53 .collect::<Vec<_>>()
54 .join(",");
55 line.push('\n');
56 line
57}
58
59#[derive(Debug)]
61pub struct RunsWriter<W: Write> {
62 dest: W,
63 kinds: Vec<ParamKind>,
65}
66
67impl<W: Write> RunsWriter<W> {
68 pub fn new(
75 mut dest: W,
76 params: &[ParamDescriptor],
77 actions: &[ActionSpec],
78 reducers: &[String],
79 ) -> io::Result<Self> {
80 dest.write_all(header_line(&column_names(params, actions, reducers)).as_bytes())?;
81 Ok(Self::appending(dest, params))
82 }
83
84 pub fn appending(dest: W, params: &[ParamDescriptor]) -> Self {
87 Self {
88 dest,
89 kinds: params.iter().map(|param| param.kind.clone()).collect(),
90 }
91 }
92
93 pub fn write_run(&mut self, outcome: &RunOutcome, config: &Config) -> io::Result<()> {
99 let run = &outcome.run;
100 let mut row = format!(
101 "{},{},{},{},{},{:016x}",
102 run.run_id, run.config_id, config.block, run.rep, run.seed, outcome.run_key
103 );
104 for (kind, value) in self.kinds.iter().zip(&config.params) {
105 row.push(',');
106 row.push_str(&escape_field(&format_value(kind, value)));
107 }
108 for tick in &config.action_ticks {
109 row.push_str(&format!(",{tick}"));
110 }
111 let timings = [outcome.build_ms, outcome.wall_ms, outcome.steps_per_s()].map(format_timing);
112 row.push_str(&format!(
113 ",{},{},{},{},{},{},{}",
114 outcome.status.as_str(),
115 outcome.stop_reason.as_str(),
116 outcome.ticks,
117 outcome.population,
118 timings[0],
119 timings[1],
120 timings[2],
121 ));
122 for value in &outcome.reducers {
123 row.push(',');
124 row.push_str(&value.map_or_else(String::new, fmt_f64));
125 }
126 row.push(',');
127 row.push_str(&escape_field(outcome.note.as_deref().unwrap_or_default()));
128 writeln!(self.dest, "{row}")
129 }
130
131 pub fn flush(&mut self) -> io::Result<()> {
137 self.dest.flush()
138 }
139
140 pub fn into_inner(mut self) -> io::Result<W> {
146 self.dest.flush()?;
147 Ok(self.dest)
148 }
149}
150
151fn format_timing(value: f64) -> String {
153 fmt_f64((value * 1000.0).round() / 1000.0)
154}
155
156#[cfg(test)]
157mod tests {
158 use henad_core::explore::measure::SeriesBuffer;
159 use henad_core::explore::outcome::{PlannedRun, RunOutcome, RunStatus, StopReason};
160 use henad_core::explore::plan::Config;
161 use henad_core::explore::spec::ActionSpec;
162 use henad_core::helpers::{choice_param, f32_param};
163 use henad_core::params::ParamValue;
164
165 use super::{RunsWriter, format_timing};
166
167 const NETWORKS: &[&str] = &["Random", "Geometric"];
168
169 #[test]
170 fn a_row_holds_every_column_in_header_order() {
171 let params = [
172 f32_param("spread, chance", "Spread chance", 0.2, 0.0, 1.0, None),
173 choice_param("network", "Network", NETWORKS, 0),
174 ];
175 let reducers = ["Infected:max".to_owned(), "Speed.[0, 1):min".to_owned()];
176 let actions = [ActionSpec {
177 name: "second, wave".to_owned(),
178 ..ActionSpec::new("seed_outbreak", 40)
179 }];
180 let mut writer = RunsWriter::new(Vec::new(), ¶ms, &actions, &reducers).expect("a vector takes every write");
181 let outcome = RunOutcome {
182 run: PlannedRun {
183 run_id: 7,
184 config_id: 2,
185 rep: 1,
186 seed: 99,
187 },
188 run_key: 0xab,
189 status: RunStatus::Panicked,
190 stop_reason: StopReason::Fault,
191 ticks: 40,
192 population: 256,
193 build_ms: 1.23456,
194 wall_ms: 8.0,
195 reducers: vec![Some(12.5), None],
196 series: SeriesBuffer::new(2),
197 note: Some("while stepping, \"x\" panicked".to_owned()),
198 };
199 let config = Config {
200 block: 1,
201 params: vec![ParamValue::F32(0.1), ParamValue::Choice(1)],
202 action_ticks: vec![25],
203 };
204 writer.write_run(&outcome, &config).expect("a vector takes every write");
205 let text = String::from_utf8(writer.into_inner().expect("a vector flushes")).expect("the rows are UTF-8");
206 let lines: Vec<&str> = text.lines().collect();
207 assert_eq!(
208 lines[0],
209 "run_id,config_id,block,rep,seed,run_key,\"spread, chance\",network,\"action.second, wave\",status,\
210 stop_reason,ticks,population,build_ms,wall_ms,steps_per_s,Infected:max,\"Speed.[0, 1):min\",note"
211 );
212 assert_eq!(
213 lines[1],
214 "7,2,1,1,99,00000000000000ab,0.1,Geometric,25,panicked,fault,40,256,1.235,8,5000,12.5,,\
215 \"while stepping, \"\"x\"\" panicked\""
216 );
217 }
218
219 #[test]
220 fn a_timing_keeps_three_decimal_places() {
221 assert_eq!(format_timing(0.1 + 0.2), "0.3");
222 assert_eq!(format_timing(2.0), "2");
223 assert_eq!(format_timing(f64::INFINITY), "");
224 assert_eq!(format_timing(f64::NAN), "");
225 }
226}