1use std::collections::BTreeMap;
9#[cfg(not(target_arch = "wasm32"))]
10use std::collections::BTreeSet;
11use std::fmt;
12use std::io::{self, BufRead};
13use std::path::{Path, PathBuf};
14
15use henad_core::explore::fingerprint::schema_hash;
16use henad_core::explore::measure::SeriesBuffer;
17use henad_core::explore::outcome::{PlannedRun, RunOutcome};
18use henad_core::explore::plan::{Config, ModelSchema, Plan, PlanError};
19use henad_core::explore::replay::Replay;
20use henad_core::explore::search::SearchSpecError;
21use henad_core::explore::spec::SweepSpec;
22
23use henad_core::explore::value::parse_value;
24
25use crate::output::manifest::{BuildRole, Manifest, ManifestError, ManifestMode, ManifestStatus, RecordedBuild};
26use crate::output::read::{ReadError, parse_one, record_ends};
27use crate::output::runs_csv::{ID_COLUMNS, NOTE_COLUMN, OUTCOME_COLUMNS};
28use crate::output::search_tables::{
29 ARCHIVE_ID_COLUMNS, EVALUATION_ID_COLUMNS, GENERATION_COLUMNS, OBJECTIVE_BATCH_COLUMNS, PATTERN_BATCH_COLUMNS,
30 RANKING_ID_COLUMNS, SearchHistory,
31};
32use crate::output::series_csv::SERIES_ID_COLUMNS;
33use crate::output::{
34 ARCHIVE_FILE, BATCHES_FILE, BEST_FILE, EVALUATIONS_FILE, GENERATIONS_FILE, MANIFEST_FILE, RUNS_FILE, SERIES_FILE,
35};
36use crate::search_run::{SearchPlan, SearchPlanError};
37use crate::spec_file::{SpecFile, SpecFileError};
38use crate::sweep::hex;
39
40#[derive(Debug, Clone)]
42pub struct ResultSet {
43 manifest: Manifest,
44 spec: SweepSpec,
45 dir: Option<PathBuf>,
47 value_columns: Vec<String>,
49 reducer_columns: Vec<String>,
50 stat_columns: Vec<String>,
51 runs: Vec<RunRow>,
52 positions: BTreeMap<u64, usize>,
54 series_bytes: usize,
56 search_tables: BTreeMap<&'static str, String>,
58}
59
60const SEARCH_FILES: [&str; 5] = [
62 EVALUATIONS_FILE,
63 BATCHES_FILE,
64 GENERATIONS_FILE,
65 BEST_FILE,
66 ARCHIVE_FILE,
67];
68
69#[derive(Debug, Clone, PartialEq)]
71pub struct RunRow {
72 pub outcome: RunOutcome,
76 pub block: usize,
78 pub values: Vec<String>,
80 pub series_held: bool,
84}
85
86impl ResultSet {
87 #[cfg(not(target_arch = "wasm32"))]
95 pub fn open_dir(dir: &Path, series_budget: usize) -> Result<Self, ResultSetError> {
96 let manifest_path = dir.join(MANIFEST_FILE);
97 if !manifest_path.exists() {
98 return Err(ResultSetError::Missing { file: MANIFEST_FILE });
99 }
100 let manifest = Manifest::read(&manifest_path).map_err(ResultSetError::Manifest)?;
101 let (runs_path, series_path) = crate::output::table_paths(dir);
102 let runs = match std::fs::read(&runs_path) {
103 Ok(runs) => runs,
104 Err(error) if error.kind() == io::ErrorKind::NotFound => {
105 return Err(ResultSetError::Missing { file: RUNS_FILE });
106 }
107 Err(source) => {
108 return Err(ResultSetError::Table(ReadError::Io {
109 path: runs_path,
110 source,
111 }));
112 }
113 };
114 let mut set = Self::from_tables(manifest, &runs, &runs_path)?;
115 set.dir = Some(dir.to_owned());
116 for file in SEARCH_FILES {
117 let path = dir.join(file);
118 match std::fs::read(&path) {
119 Ok(bytes) if !bytes.contains(&b'\n') => {}
122 Ok(bytes) => {
123 set.search_tables
124 .insert(file, String::from_utf8_lossy(&bytes).into_owned());
125 }
126 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
127 Err(source) => return Err(ResultSetError::Table(ReadError::Io { path, source })),
128 }
129 }
130 match std::fs::File::open(&series_path) {
131 Ok(file) => set.read_series(io::BufReader::new(file), &series_path, series_budget)?,
132 Err(error) if error.kind() == io::ErrorKind::NotFound => set.mark_series_missing(),
133 Err(source) => {
134 return Err(ResultSetError::Table(ReadError::Io {
135 path: series_path,
136 source,
137 }));
138 }
139 }
140 Ok(set)
141 }
142
143 pub fn from_files(files: Vec<(String, Vec<u8>)>, series_budget: usize) -> Result<Self, ResultSetError> {
157 let mut tables: BTreeMap<&'static str, Vec<u8>> = BTreeMap::new();
158 for (name, bytes) in files {
159 let Some(table) = picked_table(&name, &bytes) else {
160 continue;
161 };
162 if tables.insert(table, bytes).is_some() {
163 return Err(ResultSetError::Duplicate { file: table });
164 }
165 }
166 let manifest_bytes = tables
167 .get(MANIFEST_FILE)
168 .ok_or(ResultSetError::Missing { file: MANIFEST_FILE })?;
169 let manifest = Manifest::parse(&String::from_utf8_lossy(manifest_bytes), Path::new(MANIFEST_FILE))
170 .map_err(ResultSetError::Manifest)?;
171 let runs = tables
172 .get(RUNS_FILE)
173 .ok_or(ResultSetError::Missing { file: RUNS_FILE })?;
174 let mut set = Self::from_tables(manifest, runs, Path::new(RUNS_FILE))?;
175 match tables.get(SERIES_FILE) {
176 Some(series) => set.read_series(series.as_slice(), Path::new(SERIES_FILE), series_budget)?,
177 None => set.mark_series_missing(),
178 }
179 for file in SEARCH_FILES {
180 if let Some(bytes) = tables.get(file) {
181 set.search_tables
182 .insert(file, String::from_utf8_lossy(bytes).into_owned());
183 }
184 }
185 Ok(set)
186 }
187
188 fn from_tables(manifest: Manifest, runs: &[u8], runs_path: &Path) -> Result<Self, ResultSetError> {
190 let spec = SpecFile::from_json(&manifest.spec)
191 .and_then(SpecFile::into_spec)
192 .map_err(ResultSetError::Spec)?;
193 if let Some(search) = &spec.search {
194 search.check().map_err(ResultSetError::Search)?;
195 }
196 let stat_columns = manifest.columns.stats.clone();
197 let (column_names, records) = read_runs(runs, runs_path, stat_columns.len())?;
198 let positions = records
199 .iter()
200 .enumerate()
201 .map(|(position, run)| (run.outcome.run.run_id, position))
202 .collect();
203 Ok(Self {
204 manifest,
205 spec,
206 dir: None,
207 value_columns: column_names.value_columns,
208 reducer_columns: column_names.reducer_columns,
209 stat_columns,
210 runs: records,
211 positions,
212 series_bytes: 0,
213 search_tables: BTreeMap::new(),
214 })
215 }
216
217 fn read_series(
228 &mut self,
229 mut series: impl BufRead,
230 path: &Path,
231 series_budget: usize,
232 ) -> Result<(), ResultSetError> {
233 let width = self.stat_columns.len();
234 let row_bytes = (width + 1) * size_of::<f64>();
235 let mut line = Vec::new();
236 if !read_line(&mut series, &mut line, path)? {
237 self.mark_series_missing();
238 return Ok(());
239 }
240 check_series_header(&line, &self.stat_columns, path)?;
241 let mut budget_spent = false;
242 let mut values = Vec::with_capacity(width);
243 let mut record_number = 1;
244 let mut last_position = None;
246 while read_line(&mut series, &mut line, path)? {
247 record_number += 1;
248 let (run_id, tick) = parse_series_row(&line, width, &mut values, path, record_number)?;
249 let Some(&position) = self.positions.get(&run_id) else {
250 continue;
251 };
252 if let Some(last) = last_position.replace(position)
253 && last != position
254 {
255 self.runs[last].outcome.series.shrink_to_fit();
256 }
257 let run = &mut self.runs[position];
258 if !run.series_held {
259 continue;
260 }
261 if budget_spent || self.series_bytes + row_bytes > series_budget {
264 budget_spent = true;
265 self.series_bytes -= run.outcome.series.len() * row_bytes;
266 run.outcome.series = SeriesBuffer::new(width);
267 run.series_held = false;
268 continue;
269 }
270 self.series_bytes += row_bytes;
271 run.outcome.series.push(tick, &values);
272 }
273 if let Some(last) = last_position {
274 self.runs[last].outcome.series.shrink_to_fit();
275 }
276 Ok(())
277 }
278
279 fn mark_series_missing(&mut self) {
281 for run in &mut self.runs {
282 run.series_held = false;
283 }
284 }
285
286 pub fn manifest(&self) -> &Manifest {
288 &self.manifest
289 }
290
291 pub fn recorded_builds(&self, role: BuildRole) -> Vec<RecordedBuild> {
297 self.manifest.recorded_builds(role)
298 }
299
300 pub fn spec(&self) -> &SweepSpec {
302 &self.spec
303 }
304
305 pub fn dir(&self) -> Option<&Path> {
307 self.dir.as_deref()
308 }
309
310 pub fn value_columns(&self) -> &[String] {
314 &self.value_columns
315 }
316
317 pub fn reducer_columns(&self) -> &[String] {
321 &self.reducer_columns
322 }
323
324 pub fn stat_columns(&self) -> &[String] {
326 &self.stat_columns
327 }
328
329 pub fn runs(&self) -> &[RunRow] {
331 &self.runs
332 }
333
334 pub fn run(&self, run_id: u64) -> Option<&RunRow> {
336 self.runs.get(*self.positions.get(&run_id)?)
337 }
338
339 pub fn held_series_count(&self) -> usize {
341 self.runs.iter().filter(|run| run.series_held).count()
342 }
343
344 pub fn is_complete(&self) -> bool {
348 self.manifest.status == ManifestStatus::Complete
349 }
350
351 pub fn schema_matches(&self, schema: ModelSchema<'_>) -> bool {
356 schema.id == self.manifest.model.id && hex(schema_hash(&schema)) == self.manifest.model.schema_hash
357 }
358
359 pub fn plan(&self, schema: ModelSchema<'_>) -> Result<Plan, PlanError> {
365 self.spec.plan(&schema)
366 }
367
368 pub fn replay(&self, schema: ModelSchema<'_>, run_id: u64) -> Result<Replay, ResultReplayError> {
380 let recorded = self.run(run_id).ok_or(ResultReplayError::UnknownRun { run_id })?;
381 if self.is_search() {
382 return self.search_replay(schema, recorded);
383 }
384 let plan = self.plan(schema).map_err(ResultReplayError::Plan)?;
385 match plan.run(run_id) {
386 Some(planned)
387 if planned == recorded.outcome.run && self.key_matches(schema, plan.run_key(&planned), recorded) =>
388 {
389 plan.replay(run_id).ok_or(ResultReplayError::Mismatch { run_id })
390 }
391 _ => Err(ResultReplayError::Mismatch { run_id }),
392 }
393 }
394
395 fn search_replay(&self, schema: ModelSchema<'_>, recorded: &RunRow) -> Result<Replay, ResultReplayError> {
400 let run = recorded.outcome.run;
401 let mismatch = || ResultReplayError::Mismatch { run_id: run.run_id };
402 let plan = SearchPlan::new(&self.spec, &schema).map_err(ResultReplayError::Search)?;
403 let actions = plan.base().actions();
404 if recorded.values.len() != schema.params.len() + actions.len()
405 || self.value_columns.len() != recorded.values.len()
406 {
407 return Err(mismatch());
408 }
409 let value_of = |column: &str| {
410 let position = self.value_columns.iter().position(|name| name == column)?;
411 recorded.values.get(position)
412 };
413 let config = Config {
414 block: recorded.block,
415 params: schema
416 .params
417 .iter()
418 .map(|descriptor| parse_value(&descriptor.kind, value_of(descriptor.id)?).ok())
419 .collect::<Option<_>>()
420 .ok_or_else(mismatch)?,
421 action_ticks: actions
422 .iter()
423 .map(|action| value_of(&action.column_name())?.parse().ok())
424 .collect::<Option<_>>()
425 .ok_or_else(mismatch)?,
426 };
427 if !self.key_matches(schema, plan.run_key(&run, &config), recorded) {
428 return Err(mismatch());
429 }
430 Ok(plan.replay(&run, &config))
431 }
432
433 fn key_matches(&self, schema: ModelSchema<'_>, key: u64, recorded: &RunRow) -> bool {
436 !self.schema_matches(schema) || key == recorded.outcome.run_key
437 }
438
439 pub fn is_search(&self) -> bool {
441 self.manifest.mode == ManifestMode::Search
442 }
443
444 pub fn search_table(&self, file: &str) -> Option<&str> {
447 self.search_tables.get(file).map(String::as_str)
448 }
449
450 pub fn search_history(&self) -> Result<Option<SearchHistory>, ResultSetError> {
456 if !self.is_search() {
457 return Ok(None);
458 }
459 SearchHistory::read(
460 self.search_table(BATCHES_FILE),
461 self.search_table(GENERATIONS_FILE),
462 self.search_table(EVALUATIONS_FILE),
463 self.spec.search.as_ref(),
464 )
465 .map(Some)
466 .map_err(ResultSetError::Table)
467 }
468
469 #[cfg(not(target_arch = "wasm32"))]
477 pub fn read_run_series(
478 &self,
479 run_ids: &BTreeSet<u64>,
480 series_budget: usize,
481 ) -> Result<DirectorySeries, ResultSetError> {
482 let dir = self
483 .dir
484 .as_deref()
485 .ok_or(ResultSetError::Missing { file: SERIES_FILE })?;
486 let held: BTreeSet<u64> = run_ids
487 .iter()
488 .copied()
489 .filter(|run_id| self.positions.contains_key(run_id))
490 .collect();
491 read_directory_series(dir, &self.stat_columns, &held, series_budget)
492 }
493
494 pub fn into_runs(self) -> Vec<RunRow> {
496 self.runs
497 }
498}
499
500#[cfg(not(target_arch = "wasm32"))]
502#[derive(Debug, Clone, Default, PartialEq)]
503pub struct DirectorySeries {
504 pub series: BTreeMap<u64, SeriesBuffer>,
506 pub dropped_runs: BTreeSet<u64>,
510}
511
512#[cfg(not(target_arch = "wasm32"))]
523pub fn read_directory_series(
524 dir: &Path,
525 stat_columns: &[String],
526 run_ids: &BTreeSet<u64>,
527 series_budget: usize,
528) -> Result<DirectorySeries, ResultSetError> {
529 let (_, path) = crate::output::table_paths(dir);
530 let file = std::fs::File::open(&path).map_err(|source| {
531 ResultSetError::Table(ReadError::Io {
532 path: path.clone(),
533 source,
534 })
535 })?;
536 let mut lines = io::BufReader::new(file);
537 let mut read = DirectorySeries::default();
538 let stat_width = stat_columns.len();
539 let row_bytes = (stat_width + 1) * size_of::<f64>();
540 let mut series_bytes = 0;
541 let mut budget_spent = false;
542 let mut line = Vec::new();
543 let mut values = Vec::with_capacity(stat_width);
544 let mut record_number = 1;
545 let mut last_run = None;
547 if !read_line(&mut lines, &mut line, &path)? {
548 return Ok(read);
549 }
550 check_series_header(&line, stat_columns, &path)?;
551 while read_line(&mut lines, &mut line, &path)? {
552 record_number += 1;
553 let (run_id, tick) = parse_series_row(&line, stat_width, &mut values, &path, record_number)?;
554 if !run_ids.contains(&run_id) || read.dropped_runs.contains(&run_id) {
555 continue;
556 }
557 if let Some(last) = last_run.replace(run_id)
558 && last != run_id
559 && let Some(series) = read.series.get_mut(&last)
560 {
561 series.shrink_to_fit();
562 }
563 budget_spent |= series_bytes + row_bytes > series_budget;
564 if budget_spent {
565 if let Some(partial) = read.series.remove(&run_id) {
566 series_bytes -= partial.len() * row_bytes;
567 }
568 read.dropped_runs.insert(run_id);
569 continue;
570 }
571 series_bytes += row_bytes;
572 read.series
573 .entry(run_id)
574 .or_insert_with(|| SeriesBuffer::new(stat_width))
575 .push(tick, &values);
576 }
577 if let Some(series) = last_run.and_then(|last| read.series.get_mut(&last)) {
578 series.shrink_to_fit();
579 }
580 Ok(read)
581}
582
583fn picked_table(name: &str, bytes: &[u8]) -> Option<&'static str> {
586 let extension = Path::new(name)
587 .extension()
588 .map(|extension| extension.to_ascii_lowercase());
589 if extension.as_ref().is_some_and(|extension| extension == "json") {
590 return Some(MANIFEST_FILE);
591 }
592 let header_end = bytes.iter().position(|&byte| byte == b'\n').unwrap_or(bytes.len());
593 let header = String::from_utf8_lossy(&bytes[..header_end]);
594 let header: Vec<&str> = header.trim_end_matches('\r').split(',').collect();
595 if header.starts_with(&ID_COLUMNS) {
596 Some(RUNS_FILE)
597 } else if header.starts_with(&SERIES_ID_COLUMNS) {
598 Some(SERIES_FILE)
599 } else if header.starts_with(&EVALUATION_ID_COLUMNS) {
600 Some(EVALUATIONS_FILE)
601 } else if header == OBJECTIVE_BATCH_COLUMNS || header == PATTERN_BATCH_COLUMNS {
602 Some(BATCHES_FILE)
603 } else if header == GENERATION_COLUMNS {
604 Some(GENERATIONS_FILE)
605 } else if header.starts_with(&RANKING_ID_COLUMNS) {
606 Some(BEST_FILE)
607 } else if header.starts_with(&ARCHIVE_ID_COLUMNS) {
608 Some(ARCHIVE_FILE)
609 } else {
610 None
611 }
612}
613
614fn check_series_header(line: &[u8], stat_columns: &[String], path: &Path) -> Result<(), ResultSetError> {
616 let table_error = ResultSetError::Table;
617 let text = String::from_utf8_lossy(line);
618 let header = parse_one(&text, 0..text.len(), path, 1).map_err(table_error)?;
619 let expected = SERIES_ID_COLUMNS.len() + stat_columns.len();
620 if header.len() != expected {
621 return Err(table_error(ReadError::FieldCount {
622 path: path.to_owned(),
623 record_number: 1,
624 found: header.len(),
625 expected,
626 }));
627 }
628 let (ids, stats) = header.split_at(SERIES_ID_COLUMNS.len());
629 for (&column, name) in SERIES_ID_COLUMNS.iter().zip(ids) {
630 if name != column {
631 return Err(table_error(ReadError::MissingColumn {
632 path: path.to_owned(),
633 column,
634 }));
635 }
636 }
637 for (column, name) in stat_columns.iter().zip(stats) {
638 if name != column {
639 return Err(table_error(ReadError::BadField {
640 path: path.to_owned(),
641 record_number: 1,
642 column: column.clone(),
643 text: name.clone(),
644 }));
645 }
646 }
647 Ok(())
648}
649
650fn read_line(lines: &mut impl BufRead, line: &mut Vec<u8>, path: &Path) -> Result<bool, ResultSetError> {
654 line.clear();
655 let read = lines.read_until(b'\n', line).map_err(|source| {
656 ResultSetError::Table(ReadError::Io {
657 path: path.to_owned(),
658 source,
659 })
660 })?;
661 Ok(read > 0 && line.ends_with(b"\n"))
662}
663
664fn parse_series_row(
669 line: &[u8],
670 width: usize,
671 values: &mut Vec<f64>,
672 path: &Path,
673 record_number: usize,
674) -> Result<(u64, u64), ResultSetError> {
675 let text = String::from_utf8_lossy(line);
676 let text = text.trim_end_matches(['\n', '\r']);
677 let bad_field = |column: &str, field: &str| {
678 ResultSetError::Table(ReadError::BadField {
679 path: path.to_owned(),
680 record_number,
681 column: column.to_owned(),
682 text: field.to_owned(),
683 })
684 };
685 let mut fields = text.split(',');
686 let mut number = |column: &str| {
687 let field = fields.next().unwrap_or_default();
688 field.parse::<u64>().ok().ok_or_else(|| bad_field(column, field))
689 };
690 let run_id = number("run_id")?;
691 let tick = number("tick")?;
692 values.clear();
693 for field in fields {
694 let value = if field.is_empty() {
695 f64::NAN
696 } else {
697 field.parse().ok().ok_or_else(|| bad_field("value", field))?
698 };
699 values.push(value);
700 }
701 if values.len() != width {
702 return Err(ResultSetError::Table(ReadError::FieldCount {
703 path: path.to_owned(),
704 record_number,
705 found: values.len() + 2,
706 expected: width + 2,
707 }));
708 }
709 Ok((run_id, tick))
710}
711
712struct RunsColumnNames {
714 value_columns: Vec<String>,
715 reducer_columns: Vec<String>,
716}
717
718fn read_runs(bytes: &[u8], path: &Path, width: usize) -> Result<(RunsColumnNames, Vec<RunRow>), ResultSetError> {
723 let table_error = ResultSetError::Table;
724 let ends = record_ends(bytes);
725 let complete = ends.last().copied().unwrap_or(0);
726 let text = std::str::from_utf8(&bytes[..complete]).map_err(|error| {
727 table_error(ReadError::Io {
728 path: path.to_owned(),
729 source: io::Error::new(io::ErrorKind::InvalidData, error),
730 })
731 })?;
732 let Some((&header_end, record_ends)) = ends.split_first() else {
733 let columns = RunsColumnNames {
734 value_columns: Vec::new(),
735 reducer_columns: Vec::new(),
736 };
737 return Ok((columns, Vec::new()));
738 };
739 let header = parse_one(text, 0..header_end, path, 1).map_err(table_error)?;
740 let column_positions = RunsColumnPositions::find(&header, path).map_err(table_error)?;
741 let mut records = Vec::with_capacity(record_ends.len());
742 let mut start = header_end;
743 for (index, &end) in record_ends.iter().enumerate() {
744 let record_number = index + 2;
745 let fields = parse_one(text, start..end, path, record_number).map_err(table_error)?;
746 if fields.len() != header.len() {
747 return Err(table_error(ReadError::FieldCount {
748 path: path.to_owned(),
749 record_number,
750 found: fields.len(),
751 expected: header.len(),
752 }));
753 }
754 records.push(
755 column_positions
756 .record(&header, &fields, width, path, record_number)
757 .map_err(table_error)?,
758 );
759 start = end;
760 }
761 let columns = RunsColumnNames {
762 value_columns: header[column_positions.values.clone()].to_vec(),
763 reducer_columns: header[column_positions.reducers.clone()].to_vec(),
764 };
765 Ok((columns, records))
766}
767
768struct RunsColumnPositions {
770 values: std::ops::Range<usize>,
772 status: usize,
774 reducers: std::ops::Range<usize>,
776}
777
778impl RunsColumnPositions {
779 fn find(header: &[String], path: &Path) -> Result<Self, ReadError> {
782 let missing = |column: &'static str| ReadError::MissingColumn {
783 path: path.to_owned(),
784 column,
785 };
786 for (position, &column) in ID_COLUMNS.iter().enumerate() {
787 if header.get(position).is_none_or(|name| name != column) {
788 return Err(missing(column));
789 }
790 }
791 let status = header
792 .iter()
793 .rposition(|name| name == OUTCOME_COLUMNS[0])
794 .ok_or_else(|| missing(OUTCOME_COLUMNS[0]))?;
795 for (offset, &column) in OUTCOME_COLUMNS.iter().enumerate() {
796 if header.get(status + offset).is_none_or(|name| name != column) {
797 return Err(missing(column));
798 }
799 }
800 if header.last().is_none_or(|name| name != NOTE_COLUMN) {
801 return Err(missing(NOTE_COLUMN));
802 }
803 Ok(Self {
804 values: ID_COLUMNS.len()..status,
805 status,
806 reducers: status + OUTCOME_COLUMNS.len()..header.len() - 1,
807 })
808 }
809
810 fn record(
812 &self,
813 header: &[String],
814 fields: &[String],
815 width: usize,
816 path: &Path,
817 record_number: usize,
818 ) -> Result<RunRow, ReadError> {
819 let bad_field = |column: usize| ReadError::BadField {
820 path: path.to_owned(),
821 record_number,
822 column: header[column].clone(),
823 text: fields[column].clone(),
824 };
825 let integer = |column: usize| fields[column].parse::<u64>().ok().ok_or_else(|| bad_field(column));
826 let real = |column: usize| match fields[column].as_str() {
827 "" => Ok(f64::NAN),
828 text => text.parse::<f64>().ok().ok_or_else(|| bad_field(column)),
829 };
830 let status = self.status;
831 let reducers = self
832 .reducers
833 .clone()
834 .map(|column| match fields[column].as_str() {
835 "" => Ok(None),
836 text => text.parse::<f64>().map(Some).ok().ok_or_else(|| bad_field(column)),
837 })
838 .collect::<Result<Vec<_>, _>>()?;
839 let note = &fields[fields.len() - 1];
840 let outcome = RunOutcome {
841 run: PlannedRun {
842 run_id: integer(0)?,
843 config_id: integer(1)?,
844 rep: integer(3)?,
845 seed: integer(4)?,
846 },
847 run_key: u64::from_str_radix(&fields[5], 16).ok().ok_or_else(|| bad_field(5))?,
848 status: fields[status].parse().ok().ok_or_else(|| bad_field(status))?,
849 stop_reason: fields[status + 1].parse().ok().ok_or_else(|| bad_field(status + 1))?,
850 ticks: integer(status + 2)?,
851 population: integer(status + 3)?,
852 build_ms: real(status + 4)?,
853 wall_ms: real(status + 5)?,
854 reducers,
855 series: SeriesBuffer::new(width),
856 note: (!note.is_empty()).then(|| note.clone()),
857 };
858 Ok(RunRow {
859 outcome,
860 block: fields[2].parse().ok().ok_or_else(|| bad_field(2))?,
861 values: fields[self.values.clone()].to_vec(),
862 series_held: true,
863 })
864 }
865}
866
867#[derive(Debug)]
869pub enum ResultSetError {
870 Missing {
872 file: &'static str,
874 },
875 Duplicate {
877 file: &'static str,
879 },
880 Manifest(ManifestError),
882 Spec(SpecFileError),
884 Search(SearchSpecError),
888 Table(ReadError),
890}
891
892impl fmt::Display for ResultSetError {
893 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
894 match self {
895 Self::Missing { file } => write!(f, "the results are missing {file}"),
896 Self::Duplicate { file } => write!(f, "two selected files hold {file}"),
897 Self::Manifest(_) => f.write_str("cannot read the manifest"),
898 Self::Spec(_) => f.write_str("cannot read the spec the manifest records"),
899 Self::Search(_) => f.write_str("the search the manifest records has an invalid setting"),
900 Self::Table(_) => f.write_str("cannot read the results"),
901 }
902 }
903}
904
905impl std::error::Error for ResultSetError {
906 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
907 match self {
908 Self::Missing { .. } | Self::Duplicate { .. } => None,
909 Self::Manifest(error) => Some(error),
910 Self::Spec(error) => Some(error),
911 Self::Search(error) => Some(error),
912 Self::Table(error) => Some(error),
913 }
914 }
915}
916
917#[derive(Debug)]
919pub enum ResultReplayError {
920 Plan(PlanError),
922 Search(SearchPlanError),
924 UnknownRun {
926 run_id: u64,
928 },
929 Mismatch {
931 run_id: u64,
933 },
934}
935
936impl fmt::Display for ResultReplayError {
937 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
938 match self {
939 Self::Plan(_) | Self::Search(_) => f.write_str("the model refuses the spec of these results"),
940 Self::UnknownRun { run_id } => write!(f, "the results hold no run {run_id}"),
941 Self::Mismatch { run_id } => write!(f, "run {run_id} of the results does not match its plan"),
942 }
943 }
944}
945
946impl std::error::Error for ResultReplayError {
947 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
948 match self {
949 Self::Plan(error) => Some(error),
950 Self::Search(error) => Some(error),
951 Self::UnknownRun { .. } | Self::Mismatch { .. } => None,
952 }
953 }
954}
955
956#[cfg(test)]
957mod tests {
958 use henad_core::explore::factor::{FactorSpec, LevelSpec};
959 use henad_core::explore::plan::ModelSchema;
960 use henad_core::explore::replay::Replay;
961 use henad_core::explore::search::{Aggregate, Goal, Objective, SearchAlgorithm, SearchSpec};
962 use henad_core::explore::spec::SweepSpec;
963 use henad_core::params::{ParamDescriptor, ParamValue};
964
965 use super::ResultSet;
966 use crate::progress::NoProgress;
967 use crate::tests::support::{ScratchDir, entry, sweep_options, sweep_with};
968
969 fn value_of(replay: &Replay, params: &[ParamDescriptor], id: &str) -> ParamValue {
971 let position = params
972 .iter()
973 .position(|param| param.id == id)
974 .expect("the schema has the parameter");
975 replay.params[position].clone()
976 }
977
978 #[test]
979 fn a_search_run_replays_each_value_under_its_parameter_id() {
980 let sir = entry("sir", None);
981 let mut spec = SweepSpec::new("sir");
982 spec.fixed = vec![
983 ("grid_width".to_owned(), "8".to_owned()),
984 ("grid_height".to_owned(), "8".to_owned()),
985 ];
986 spec.run.steps = 4;
987 spec.measure.default_reducers = false;
988 spec.measure.reducers = vec!["Infected:max".parse().expect("a valid reducer")];
989 let rate = |min, max| LevelSpec::Range { min, max, step: None };
990 spec.search = Some(SearchSpec {
991 algorithm: SearchAlgorithm::Random,
992 max_evaluations: 2,
993 batch_size: 2,
994 objective: Some(Objective {
995 column: "Infected:max".to_owned(),
996 goal: Goal::Minimize,
997 aggregate: Aggregate::Median,
998 }),
999 space: vec![
1000 FactorSpec::param("infection_rate", rate(0.5, 0.9)),
1001 FactorSpec::param("recovery_rate", rate(0.02, 0.3)),
1002 ],
1003 });
1004 let scratch = ScratchDir::new("search-replay-by-id");
1005 sweep_with(
1006 &sir,
1007 None,
1008 &spec,
1009 scratch.path(),
1010 &sweep_options(false),
1011 &mut NoProgress,
1012 )
1013 .expect("the search runs");
1014 let set = ResultSet::open_dir(scratch.path(), usize::MAX).expect("the folder reads back");
1015 let schema = sir.schema();
1016 let replay = set.replay(schema, 0).expect("the run replays");
1017
1018 let mut params = schema.params.to_vec();
1020 let position = |id: &str| params.iter().position(|param| param.id == id).expect("a SIR parameter");
1021 let (infection, recovery) = (position("infection_rate"), position("recovery_rate"));
1022 params.swap(infection, recovery);
1023 let reordered = ModelSchema {
1024 params: ¶ms,
1025 ..schema
1026 };
1027 let swapped = set.replay(reordered, 0).expect("the run replays");
1028 for id in ["infection_rate", "recovery_rate", "grid_width"] {
1029 assert_eq!(
1030 value_of(&swapped, ¶ms, id),
1031 value_of(&replay, schema.params, id),
1032 "{id}"
1033 );
1034 }
1035 }
1036}