Skip to main content

bloqade_lanes_bytecode/policy/
eval.rs

1//! `eval-policy` subcommand: run-once with summary output.
2
3use std::path::Path;
4use std::sync::Arc;
5use std::time::Instant;
6
7use bloqade_lanes_dsl_core::sandbox::SandboxConfig;
8use bloqade_lanes_search::dsl::fixture::{self, Problem};
9use bloqade_lanes_search::dsl::move_policy_dsl::{
10    NoOpMoveObserver, PolicyOptions, PolicyStatus, solve_with_policy,
11};
12use bloqade_lanes_search::dsl::target_generator_dsl::{NoOpTargetObserver, run_target_policy};
13use bloqade_lanes_search::primitives::lane_index::LaneIndex;
14
15use super::output::{EvalEnvelope, TargetEvalEnvelope, print_human_move, print_human_target};
16
17const SCHEMA_VERSION: u32 = 1;
18
19pub fn run_eval_policy(
20    policy: &Path,
21    problem: &Path,
22    params: Option<&Path>,
23    max_expansions: Option<u64>,
24    timeout_s: Option<f64>,
25    json: bool,
26    _seed: Option<u64>,
27) -> Result<(), String> {
28    let (parsed, arch_path) = fixture::load(problem).map_err(|e| format!("error: {e}"))?;
29    let arch_json = std::fs::read_to_string(&arch_path)
30        .map_err(|e| format!("error: reading arch {}: {e}", arch_path.display()))?;
31    let arch = bloqade_lanes_bytecode_core::arch::ArchSpec::from_json(&arch_json)
32        .map_err(|e| format!("error: parsing arch {}: {e}", arch_path.display()))?;
33
34    match parsed {
35        Problem::Move(mp) => {
36            let exit = run_move(
37                policy,
38                problem,
39                mp,
40                arch,
41                params,
42                max_expansions,
43                timeout_s,
44                json,
45            )?;
46            std::process::exit(exit);
47        }
48        Problem::Target(tp) => {
49            let exit = run_target(policy, problem, tp, arch, params, json)?;
50            std::process::exit(exit);
51        }
52    }
53}
54
55#[allow(clippy::too_many_arguments)]
56fn run_move(
57    policy: &Path,
58    problem: &Path,
59    mp: bloqade_lanes_search::dsl::fixture::MoveProblem,
60    arch: bloqade_lanes_bytecode_core::arch::ArchSpec,
61    params: Option<&Path>,
62    max_expansions: Option<u64>,
63    timeout_s: Option<f64>,
64    json: bool,
65) -> Result<i32, String> {
66    let index = Arc::new(LaneIndex::new(arch));
67    let initial = mp.initial_locations();
68    let target = mp.target_locations();
69    let blocked = mp.blocked_locations();
70
71    let opts = PolicyOptions {
72        policy_path: policy.display().to_string(),
73        sandbox: SandboxConfig::default(),
74        policy_params: load_params(params, &mp.policy_params)?,
75        max_expansions: max_expansions
76            .or(mp.budget.as_ref().map(|b| b.max_expansions))
77            .unwrap_or(5_000),
78        timeout_s: Some(
79            timeout_s
80                .or(mp.budget.as_ref().map(|b| b.timeout_s))
81                .unwrap_or(10.0),
82        ),
83    };
84    let mut obs = NoOpMoveObserver;
85    let t0 = Instant::now();
86    let res = solve_with_policy(initial, target, blocked, index, opts, &mut obs)
87        .map_err(|e| format!("error: {e}"))?;
88    let wall_ms = t0.elapsed().as_secs_f64() * 1000.0;
89
90    let status_str = res.status.as_label();
91    let halt_reason = halt_reason(&res.status);
92    let env = EvalEnvelope {
93        v: SCHEMA_VERSION,
94        kind: "move",
95        policy: policy.to_str().unwrap_or(""),
96        problem: problem.to_str().unwrap_or(""),
97        status: status_str,
98        halt_reason: halt_reason.as_deref(),
99        expansions: res.nodes_expanded as u64,
100        max_depth: res.move_layers.len() as u32,
101        wall_time_ms: wall_ms,
102    };
103    if json {
104        println!("{}", serde_json::to_string(&env).unwrap());
105    } else {
106        print_human_move(&env);
107    }
108    Ok(exit_code(&res.status))
109}
110
111fn run_target(
112    policy: &Path,
113    problem: &Path,
114    tp: bloqade_lanes_search::dsl::fixture::TargetProblem,
115    arch: bloqade_lanes_bytecode_core::arch::ArchSpec,
116    params: Option<&Path>,
117    json: bool,
118) -> Result<i32, String> {
119    let index = Arc::new(LaneIndex::new(arch));
120    let placement = tp.current_placement_locations();
121    let cfg = SandboxConfig::default();
122    let mut obs = NoOpTargetObserver;
123    let t0 = Instant::now();
124    let result = run_target_policy(
125        policy,
126        index,
127        placement,
128        tp.controls.clone(),
129        tp.targets.clone(),
130        tp.lookahead_cz_layers.clone(),
131        tp.cz_stage_index,
132        load_params(params, &tp.policy_params)?,
133        &cfg,
134        &mut obs,
135    );
136    let wall_ms = t0.elapsed().as_secs_f64() * 1000.0;
137
138    let (num_candidates, first_candidate_size) = match &result {
139        Ok(cands) => (cands.len(), cands.first().map_or(0, |c| c.len())),
140        Err(_) => (0, 0),
141    };
142    let env = TargetEvalEnvelope {
143        v: SCHEMA_VERSION,
144        kind: "target",
145        policy: policy.to_str().unwrap_or(""),
146        problem: problem.to_str().unwrap_or(""),
147        ok: result.is_ok(),
148        num_candidates,
149        first_candidate_size,
150        wall_time_ms: wall_ms,
151    };
152    if json {
153        println!("{}", serde_json::to_string(&env).unwrap());
154    } else {
155        print_human_target(&env);
156    }
157    Ok(if result.is_ok() { 0 } else { 2 })
158}
159
160pub(crate) fn load_params(
161    file: Option<&Path>,
162    fallback: &serde_json::Value,
163) -> Result<serde_json::Value, String> {
164    match file {
165        Some(p) => {
166            let bytes =
167                std::fs::read(p).map_err(|e| format!("error: reading {}: {e}", p.display()))?;
168            serde_json::from_slice(&bytes)
169                .map_err(|e| format!("error: parsing {}: {e}", p.display()))
170        }
171        None => Ok(fallback.clone()),
172    }
173}
174
175fn halt_reason(s: &PolicyStatus) -> Option<String> {
176    match s {
177        PolicyStatus::Solved => Some("policy_halt".into()),
178        PolicyStatus::Fallback(r) => Some(r.clone()),
179        _ => None,
180    }
181}
182
183fn exit_code(s: &PolicyStatus) -> i32 {
184    match s {
185        PolicyStatus::Solved => 0,
186        PolicyStatus::BudgetExhausted
187        | PolicyStatus::Timeout
188        | PolicyStatus::Fallback(_)
189        | PolicyStatus::Unsolvable => 2,
190        _ => 1,
191    }
192}