Skip to main content

optimization_job_api/legacy/
result.rs

1use 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    // Link only artifacts that exist: a job cancelled while waiting for CPU
74    // slots never created a journal.
75    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}
97/// Comparison preserves all arms and labels comparability; never invents a winner.
98pub 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}