bloqade_lanes_bytecode/policy/
eval.rs1use 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}