optimization_job_api/legacy/
result.rs1use crate::{Result, legacy::JobSpec, storage};
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4use std::{collections::BTreeSet, path::Path};
5#[derive(Clone, Debug, Serialize, Deserialize)]
6#[serde(deny_unknown_fields)]
7pub struct RetryRecord {
8 pub retry_id: usize,
9 pub run_seed: u64,
10 pub spec_digest: String,
11 pub initialization: String,
12 pub x: Vec<f64>,
13 pub y: Option<f64>,
14 pub actual_evaluations: u64,
15 pub elapsed_seconds: f64,
16 pub valid: bool,
17 pub feasible: bool,
18}
19pub fn replay(dir: &Path, spec: &JobSpec) -> Result<Vec<RetryRecord>> {
20 let path = dir.join("retries.jsonl");
21 if !path.exists() {
22 return Ok(vec![]);
23 }
24 let records: Vec<RetryRecord> = storage::read_checked(&path, 1)?;
25 let digest = spec.digest()?;
26 let mut ids = BTreeSet::new();
27 for r in &records {
28 if !ids.insert(r.retry_id)
29 || r.retry_id >= spec.budget.retries
30 || r.spec_digest != digest
31 || r.x.len() != spec.problem.config.dimension
32 || r.x.iter().any(|x| !x.is_finite())
33 || r.x
34 .iter()
35 .zip(
36 spec.problem
37 .bounds
38 .lower
39 .iter()
40 .zip(&spec.problem.bounds.upper),
41 )
42 .any(|(x, (l, u))| x < l || x > u)
43 || !r.elapsed_seconds.is_finite()
44 || r.elapsed_seconds < 0.0
45 || r.y.is_some_and(|y| !y.is_finite())
46 || r.valid != r.y.is_some()
47 || (r.feasible && !r.valid)
48 || r.actual_evaluations == 0
49 || r.actual_evaluations > spec.maximum_evaluations()? / (spec.budget.retries as u64)
50 {
51 return Err("invalid or duplicate retry journal record".into());
52 }
53 }
54 Ok(records)
55}
56pub fn manifest(
57 dir: &Path,
58 spec: &JobSpec,
59 status: &str,
60 complete: bool,
61 elapsed: f64,
62 worker_digest: &str,
63) -> Result<Value> {
64 let records = replay(dir, spec)?;
65 let known = records.iter().try_fold(0u64, |sum, r| {
66 sum.checked_add(r.actual_evaluations)
67 .ok_or("evaluation sum overflow")
68 })?;
69 let best = records
70 .iter()
71 .filter(|r| r.feasible)
72 .min_by(|a, b| a.y.unwrap().total_cmp(&b.y.unwrap()));
73 let artifacts = if dir.join("retries.jsonl").exists() {
76 json!({"retries":"retries.jsonl"})
77 } else {
78 json!({})
79 };
80 Ok(
81 json!({"schema_version":1,"tutorial":"fcmaes-native-worker","formulation":"single_objective",
82 "command":"fcmaes-native-worker execute JOB_DIRECTORY", "status":status,
83 "seed":spec.execution.seed,"workers":spec.execution.workers,
84 "requested_evaluations":spec.requested()?,"actual_evaluations":if complete {Some(known)}else{None},
85 "elapsed_seconds":elapsed,"objectives":[{"column":"objective","label":spec.problem.id,"unit":if spec.problem.id=="gtop.cassini1" {"km/s"}else{"dimensionless"}}],
86 "descriptors":[],"feasible_results":records.iter().filter(|r|r.feasible).count(),
87 "best":best.map(|r|json!({"x":r.x,"y":r.y,"retry_id":r.retry_id})),
88 "artifacts":artifacts,
89 "runner":{"profile":"fcmaes.runner/0.1","spec_digest":spec.digest()?,"worker_digest":worker_digest,
90 "problem":spec.problem,"optimizer":spec.optimizer,"budget":spec.budget,"execution":spec.execution,
91 "termination_reason":status,"accounting_complete":complete,"known_evaluations_lower_bound":known,
92 "completed_retries":records.len(),"constraints":"none; finite objective required","data":"built-in",
93 "initialization":"uniform bounds; solver seed=retry_run_seed(root_seed,global_retry_id); runid=0"}
94 }),
95 )
96}
97pub fn compare(runs: &[Value]) -> Result<Value> {
99 if runs.len() < 2 {
100 return Err("comparison needs at least two manifests".into());
101 }
102 for r in runs {
103 if r["schema_version"] != 1
104 || (!r["runner"]["profile"].is_null() && !r["objectives"].is_array())
105 || !r["tutorial"].is_string()
106 || !r["formulation"].is_string()
107 {
108 return Err("not a run.json v1 manifest".into());
109 }
110 }
111 let first = &runs[0];
112 let mut warnings = Vec::<String>::new();
113 for r in &runs[1..] {
114 for key in ["tutorial", "formulation", "objectives"] {
115 if r[key] != first[key] {
116 return Err(format!("incompatible {key}").into());
117 }
118 }
119 if r["runner"]["profile"].is_null() || first["runner"]["profile"].is_null() {
120 warnings.push("Legacy manifest: compatibility of data, transformations and constraints requires domain review; descriptive comparison only".into());
121 } else {
122 for key in ["problem", "data", "constraints"] {
123 if r["runner"][key] != first["runner"][key] {
124 return Err(format!("incompatible runner.{key}").into());
125 }
126 }
127 if r["runner"]["worker_digest"] != first["runner"]["worker_digest"] {
128 warnings.push("Different worker binaries; review code provenance".into());
129 }
130 }
131 if r["seed"] != first["seed"] {
132 warnings.push("Seeds differ; arms are unpaired".into());
133 }
134 if r["workers"] != first["workers"] {
135 warnings.push("Worker allocations differ".into());
136 }
137 let mode = &r["runner"]["budget"]["evaluation_limit_mode"];
138 if !mode.is_null() && *mode != first["runner"]["budget"]["evaluation_limit_mode"] {
139 warnings.push(
140 "Evaluation limit modes differ (hard vs soft); compare actual, not requested, work"
141 .into(),
142 );
143 }
144 }
145 if first["formulation"] != "single_objective" {
146 warnings.push("No automatic Pareto or hypervolume comparison; common scaling/reference definitions required".into());
147 }
148 for r in runs {
149 if r["actual_evaluations"].is_null() {
150 warnings.push(
151 "At least one arm has incomplete accounting (forced stop); its actual work is only a lower bound"
152 .into(),
153 );
154 }
155 }
156 warnings.sort();
157 warnings.dedup();
158 let budget = |r: &Value| r["runner"]["budget"]["wall_time_seconds"].clone();
159 let summary: Vec<Value> = runs
160 .iter()
161 .enumerate()
162 .map(|(arm, r)| {
163 json!({"arm":arm,"optimizer":r["runner"]["optimizer"]["kind"],
164 "evaluation_limit_mode":r["runner"]["budget"]["evaluation_limit_mode"],
165 "status":r["status"],"seed":r["seed"],"workers":r["workers"],
166 "requested_evaluations":r["requested_evaluations"],"actual_evaluations":r["actual_evaluations"],
167 "known_evaluations_lower_bound":r["runner"]["known_evaluations_lower_bound"],
168 "completed_retries":r["runner"]["completed_retries"],"feasible_results":r["feasible_results"],
169 "best_y":r["best"]["y"],"elapsed_seconds":r["elapsed_seconds"]})
170 })
171 .collect();
172 Ok(
173 json!({"summary":summary,"arms":runs,"equal_requested_work":runs.iter().all(|r|r["requested_evaluations"]==first["requested_evaluations"]),
174 "equal_actual_work":!first["actual_evaluations"].is_null() && runs.iter().all(|r|r["actual_evaluations"]==first["actual_evaluations"]),
175 "equal_wall_time_budget":!budget(first).is_null() && runs.iter().all(|r|budget(r)==budget(first)),
176 "warnings":warnings,"interpretation":"Descriptive arms; no statistical winner inferred. Incomplete and zero-feasible arms retained."}),
177 )
178}