#![allow(
clippy::similar_names,
clippy::too_many_lines,
clippy::cast_precision_loss,
clippy::type_complexity
)]
use std::collections::{BTreeMap, HashMap, HashSet};
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::Instant;
#[derive(Debug, Clone)]
struct BksRecord {
vehicles: i32,
distance: f64,
}
#[derive(Debug, Clone)]
struct RunResult {
instance: String,
seconds: f64,
vehicles: Option<i32>,
distance: Option<f64>,
main_iterations: Option<u64>,
ges_runs: Option<u64>,
ges_inner_iterations: Option<u64>,
return_code: i32,
error: Option<String>,
}
#[derive(Debug, Clone)]
struct Comparison {
status: &'static str,
dveh: Option<i32>,
ddist_pct: Option<f64>,
bks_text: String,
}
#[derive(Debug, Clone, Copy)]
enum SeedMode {
Offset,
Fixed,
}
#[derive(Debug, Clone)]
struct Args {
instances: Vec<String>,
groups: Vec<String>,
time_limit: u64,
seed: u64,
seed_mode: SeedMode,
jobs: usize,
bin: String,
release: bool,
bks_file: String,
initial_method: String,
}
enum ParseResult {
Ok(Args),
Help,
}
fn default_jobs() -> usize {
thread::available_parallelism()
.map_or(1, std::num::NonZero::get)
.max(1)
}
fn print_help(program: &str) {
println!("Usage: {program} [options]");
println!();
println!("Options:");
println!(" --instances <list...> Specific instances (name, stem, or path)");
println!(" --groups <list...> Groups/patterns: LC1 LR2 LRC1 ALL '*_8_*'");
println!(" --time-limit <seconds> Per-instance time limit (default: 300)");
println!(" --seed <int> Base seed (default: 42)");
println!(" --seed-mode <mode> offset|fixed (default: offset)");
println!(" --jobs <int> Parallel workers (default: CPU cores)");
println!(" --bin <path> Solver binary (default: target/release/pdp_lns)");
println!(" --release Build release binary if missing");
println!(" --bks-file <path> BKS file (default: instances/bks_all.txt)");
println!(
" --initial-method <mode> legacy|lu2006|ropke2006|cluster2004|hosny2012 (default: legacy)"
);
println!(" -h, --help Show this help");
}
fn parse_seed_mode(raw: &str) -> Result<SeedMode, String> {
match raw.to_ascii_lowercase().as_str() {
"offset" => Ok(SeedMode::Offset),
"fixed" => Ok(SeedMode::Fixed),
_ => Err(format!(
"invalid --seed-mode '{raw}', expected offset|fixed"
)),
}
}
fn parse_args() -> Result<ParseResult, String> {
let argv: Vec<String> = std::env::args().collect();
let program = argv.first().map_or("benchmark_lilim", String::as_str);
let mut args = Args {
instances: Vec::new(),
groups: Vec::new(),
time_limit: 300,
seed: 42,
seed_mode: SeedMode::Offset,
jobs: default_jobs(),
bin: "target/release/pdp_lns".to_string(),
release: false,
bks_file: "instances/bks_all.txt".to_string(),
initial_method: "legacy".to_string(),
};
let mut i = 1usize;
while i < argv.len() {
let token = &argv[i];
match token.as_str() {
"-h" | "--help" => {
print_help(program);
return Ok(ParseResult::Help);
}
"--release" => {
args.release = true;
i += 1;
}
"--time-limit" => {
i += 1;
let v = argv
.get(i)
.ok_or_else(|| "missing value for --time-limit".to_string())?;
args.time_limit = v
.parse::<u64>()
.map_err(|_| format!("invalid --time-limit '{v}'"))?;
i += 1;
}
"--seed" => {
i += 1;
let v = argv
.get(i)
.ok_or_else(|| "missing value for --seed".to_string())?;
args.seed = v
.parse::<u64>()
.map_err(|_| format!("invalid --seed '{v}'"))?;
i += 1;
}
"--seed-mode" => {
i += 1;
let v = argv
.get(i)
.ok_or_else(|| "missing value for --seed-mode".to_string())?;
args.seed_mode = parse_seed_mode(v)?;
i += 1;
}
"--jobs" => {
i += 1;
let v = argv
.get(i)
.ok_or_else(|| "missing value for --jobs".to_string())?;
args.jobs = v
.parse::<usize>()
.map_err(|_| format!("invalid --jobs '{v}'"))?
.max(1);
i += 1;
}
"--bin" => {
i += 1;
let v = argv
.get(i)
.ok_or_else(|| "missing value for --bin".to_string())?;
args.bin.clone_from(v);
i += 1;
}
"--bks-file" => {
i += 1;
let v = argv
.get(i)
.ok_or_else(|| "missing value for --bks-file".to_string())?;
args.bks_file.clone_from(v);
i += 1;
}
"--initial-method" => {
i += 1;
let v = argv
.get(i)
.ok_or_else(|| "missing value for --initial-method".to_string())?;
let low = v.to_ascii_lowercase();
if low != "legacy"
&& low != "lu2006"
&& low != "ropke2006"
&& low != "ropke"
&& low != "cluster2004"
&& low != "cluster"
&& low != "hosny2012"
&& low != "hosny"
&& low != "best2012"
{
return Err(format!(
"invalid --initial-method '{v}', expected legacy|lu2006|ropke2006|cluster2004|hosny2012"
));
}
args.initial_method = if low == "ropke" {
"ropke2006".to_string()
} else if low == "cluster" {
"cluster2004".to_string()
} else if low == "hosny" || low == "best2012" {
"hosny2012".to_string()
} else {
low
};
i += 1;
}
"--instances" => {
i += 1;
let start = i;
while i < argv.len() && !argv[i].starts_with("--") {
args.instances.push(argv[i].clone());
i += 1;
}
if i == start {
return Err("--instances requires at least one value".to_string());
}
}
"--groups" => {
i += 1;
let start = i;
while i < argv.len() && !argv[i].starts_with("--") {
args.groups.push(argv[i].clone());
i += 1;
}
if i == start {
return Err("--groups requires at least one value".to_string());
}
}
_ => {
return Err(format!("unknown argument '{token}'. Use --help"));
}
}
}
Ok(ParseResult::Ok(args))
}
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn instances_dir() -> PathBuf {
repo_root().join("instances")
}
fn stem_lower(path: &Path) -> String {
path.file_stem()
.and_then(|s| s.to_str())
.map_or_else(String::new, str::to_ascii_lowercase)
}
fn discover_instance_files() -> Result<Vec<PathBuf>, String> {
let dir = instances_dir();
let mut files = Vec::new();
let entries =
fs::read_dir(&dir).map_err(|e| format!("failed to read {}: {e}", dir.display()))?;
for entry in entries {
let path = entry
.map_err(|e| format!("failed to read dir entry: {e}"))?
.path();
if path.extension() != Some(OsStr::new("txt")) {
continue;
}
let name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| format!("invalid UTF-8 file name: {}", path.display()))?;
if name == "bks.txt"
|| name.starts_with("bks_")
|| name.starts_with("result_")
|| name.starts_with("test_")
{
continue;
}
files.push(path);
}
files.sort_by_key(|p| stem_lower(p));
Ok(files)
}
fn stem_matches_prefix_digits(stem: &str, prefix: &str) -> bool {
if !stem.starts_with(prefix) {
return false;
}
stem.len() > prefix.len() && stem[prefix.len()..].chars().all(|c| c.is_ascii_digit())
}
fn wildcard_match_case_insensitive(pattern: &str, text: &str) -> bool {
let p = pattern.to_ascii_lowercase().chars().collect::<Vec<_>>();
let t = text.to_ascii_lowercase().chars().collect::<Vec<_>>();
let (mut pi, mut ti) = (0usize, 0usize);
let mut star: Option<usize> = None;
let mut match_i = 0usize;
while ti < t.len() {
if pi < p.len() && (p[pi] == '?' || p[pi] == t[ti]) {
pi += 1;
ti += 1;
} else if pi < p.len() && p[pi] == '*' {
star = Some(pi);
pi += 1;
match_i = ti;
} else if let Some(s) = star {
pi = s + 1;
match_i += 1;
ti = match_i;
} else {
return false;
}
}
while pi < p.len() && p[pi] == '*' {
pi += 1;
}
pi == p.len()
}
fn resolve_instances(
instance_args: &[String],
group_args: &[String],
) -> Result<Vec<PathBuf>, String> {
let all_files = discover_instance_files()?;
if instance_args.is_empty() && group_args.is_empty() {
return Ok(all_files);
}
let mut by_stem_ci = HashMap::<String, PathBuf>::new();
let mut by_name_ci = HashMap::<String, PathBuf>::new();
for p in &all_files {
let stem = p
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("invalid stem UTF-8: {}", p.display()))?
.to_ascii_lowercase();
let name = p
.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("invalid file name UTF-8: {}", p.display()))?
.to_ascii_lowercase();
by_stem_ci.insert(stem, p.clone());
by_name_ci.insert(name, p.clone());
}
let mut selected = HashSet::<PathBuf>::new();
for raw in instance_args {
let p = PathBuf::from(raw);
if p.is_file() {
selected.insert(fs::canonicalize(&p).unwrap_or(p));
continue;
}
let raw_stem = Path::new(raw)
.file_stem()
.and_then(|s| s.to_str())
.map_or_else(String::new, str::to_ascii_lowercase);
if let Some(found) = by_stem_ci.get(&raw_stem) {
selected.insert(found.clone());
continue;
}
let raw_name = Path::new(raw)
.file_name()
.and_then(|s| s.to_str())
.map_or_else(String::new, str::to_ascii_lowercase);
if let Some(found) = by_name_ci.get(&raw_name) {
selected.insert(found.clone());
continue;
}
let local = instances_dir().join(raw);
if local.is_file() {
selected.insert(local);
continue;
}
let local_txt = instances_dir().join(format!("{raw_stem}.txt"));
if local_txt.is_file() {
selected.insert(local_txt);
continue;
}
return Err(format!("Cannot resolve instance '{raw}'"));
}
let stems: Vec<String> = all_files.iter().map(|p| stem_lower(p)).collect();
for raw in group_args {
let g = raw.trim();
let g_upper = g.to_ascii_uppercase();
let g_lower = g.to_ascii_lowercase();
let matched: Vec<PathBuf> = if g_upper == "ALL" {
all_files.clone()
} else if g_upper == "ALL_BKS" {
all_files
.iter()
.filter(|p| {
let s = stem_lower(p);
s == "lc101" || s == "lr101" || s == "lrc101"
})
.cloned()
.collect()
} else if matches!(
g_upper.as_str(),
"LC1" | "LC2" | "LR1" | "LR2" | "LRC1" | "LRC2"
) {
let prefix = format!("{g_upper}_");
all_files
.iter()
.filter(|p| {
p.file_stem()
.and_then(|s| s.to_str())
.is_some_and(|s| s.to_ascii_uppercase().starts_with(&prefix))
})
.cloned()
.collect()
} else if matches!(g_lower.as_str(), "lc" | "lr" | "lrc") {
all_files
.iter()
.filter(|p| stem_matches_prefix_digits(&stem_lower(p), &g_lower))
.cloned()
.collect()
} else {
all_files
.iter()
.filter(|p| {
p.file_stem()
.and_then(|s| s.to_str())
.is_some_and(|s| wildcard_match_case_insensitive(g, s))
})
.cloned()
.collect()
};
if matched.is_empty() {
let preview = stems
.iter()
.take(10)
.cloned()
.collect::<Vec<_>>()
.join(", ");
return Err(format!(
"Group/pattern '{raw}' matched 0 instances. Examples available: {preview}"
));
}
for p in matched {
selected.insert(p);
}
}
let mut out: Vec<PathBuf> = selected.into_iter().collect();
out.sort_by_key(|p| stem_lower(p));
Ok(out)
}
fn parse_bks(path: &Path) -> Result<HashMap<String, BksRecord>, String> {
if !path.exists() {
return Ok(HashMap::new());
}
let text = fs::read_to_string(path)
.map_err(|e| format!("failed to read BKS file {}: {e}", path.display()))?;
let mut data = HashMap::<String, BksRecord>::new();
for line in text.lines() {
let parts = line.split_whitespace().collect::<Vec<_>>();
if parts.len() < 3 {
continue;
}
let key = parts[0].to_ascii_lowercase();
if !(key.starts_with("lc") || key.starts_with("lr") || key.starts_with("lrc")) {
continue;
}
let Ok(vehicles) = parts[1].parse::<i32>() else {
continue;
};
let Ok(distance) = parts[2].replace(',', ".").parse::<f64>() else {
continue;
};
let rec = BksRecord { vehicles, distance };
let keep = match data.get(&key) {
None => true,
Some(prev) => {
rec.vehicles < prev.vehicles
|| (rec.vehicles == prev.vehicles && rec.distance < prev.distance)
}
};
if keep {
data.insert(key, rec);
}
}
Ok(data)
}
fn ensure_binary(bin_path: &Path, release: bool) -> Result<(), String> {
if bin_path.exists() {
return Ok(());
}
if !release {
return Err(format!(
"Binary '{}' does not exist. Build it first or pass --release.",
bin_path.display()
));
}
println!("[build] cargo build --release");
let status = Command::new("cargo")
.arg("build")
.arg("--release")
.current_dir(repo_root())
.status()
.map_err(|e| format!("failed to start cargo build --release: {e}"))?;
if !status.success() {
return Err("cargo build --release failed".to_string());
}
Ok(())
}
fn parse_solver_output(
output: &str,
) -> (
Option<i32>,
Option<f64>,
Option<u64>,
Option<u64>,
Option<u64>,
) {
let mut vehicles = None;
let mut distance = None;
let mut main_iterations = None;
let mut ges_runs = None;
let mut ges_inner_iterations = None;
for line in output.lines() {
let trimmed = line.trim();
if let Some(v) = line.strip_prefix("Vehicles:") {
vehicles = v.trim().parse::<i32>().ok();
}
if let Some(v) = line.strip_prefix("Distance:") {
distance = v.trim().parse::<f64>().ok();
}
if let Some((before, _)) = trimmed.split_once(" iterations in ")
&& let Some(tok) = before.split_whitespace().last()
&& let Ok(v) = tok.parse::<u64>()
{
main_iterations = Some(v);
}
if let Some(rest) = trimmed.strip_prefix("GES_TOTAL:") {
for part in rest.split(',') {
let Some((k, v)) = part.trim().split_once('=') else {
continue;
};
let val = v.trim().parse::<u64>().ok();
match k.trim() {
"runs" => ges_runs = val,
"inner_iters" => ges_inner_iterations = val,
_ => {}
}
}
}
}
(
vehicles,
distance,
main_iterations,
ges_runs,
ges_inner_iterations,
)
}
fn run_one(
bin_path: &Path,
instance_path: &Path,
time_limit: u64,
seed: u64,
initial_method: &str,
) -> RunResult {
let start = Instant::now();
let output = Command::new(bin_path)
.arg(instance_path)
.arg(time_limit.to_string())
.arg(seed.to_string())
.arg(initial_method)
.current_dir(repo_root())
.env("GES_THREADS", "1")
.output();
let elapsed = start.elapsed().as_secs_f64();
let instance = instance_path
.file_stem()
.and_then(|s| s.to_str())
.map_or_else(|| "<invalid>".to_string(), std::string::ToString::to_string);
match output {
Ok(out) => {
let mut merged = String::new();
merged.push_str(&String::from_utf8_lossy(&out.stdout));
merged.push('\n');
merged.push_str(&String::from_utf8_lossy(&out.stderr));
let (vehicles, distance, main_iterations, ges_runs, ges_inner_iterations) =
parse_solver_output(&merged);
let code = out.status.code().unwrap_or(-1);
let error = if !out.status.success() {
Some(format!("solver exited with code {code}"))
} else if vehicles.is_none() || distance.is_none() {
Some("failed to parse Vehicles/Distance from solver output".to_string())
} else {
None
};
RunResult {
instance,
seconds: elapsed,
vehicles,
distance,
main_iterations,
ges_runs,
ges_inner_iterations,
return_code: code,
error,
}
}
Err(e) => RunResult {
instance,
seconds: elapsed,
vehicles: None,
distance: None,
main_iterations: None,
ges_runs: None,
ges_inner_iterations: None,
return_code: -1,
error: Some(format!("failed to start solver: {e}")),
},
}
}
fn compare_to_bks(result: &RunResult, bks: Option<&BksRecord>) -> Comparison {
if result.error.is_some() {
return Comparison {
status: "FAILED",
dveh: None,
ddist_pct: None,
bks_text: "-".to_string(),
};
}
let Some(bks) = bks else {
return Comparison {
status: "NO_BKS",
dveh: None,
ddist_pct: None,
bks_text: "-".to_string(),
};
};
let Some(veh) = result.vehicles else {
return Comparison {
status: "NO_BKS",
dveh: None,
ddist_pct: None,
bks_text: "-".to_string(),
};
};
let Some(dist) = result.distance else {
return Comparison {
status: "NO_BKS",
dveh: None,
ddist_pct: None,
bks_text: "-".to_string(),
};
};
let dveh = veh - bks.vehicles;
let ddist_pct = ((dist - bks.distance) / bks.distance) * 100.0;
let status = if veh < bks.vehicles || (veh == bks.vehicles && dist < bks.distance - 1e-6) {
"BETTER"
} else if veh == bks.vehicles && (dist - bks.distance).abs() <= 1e-6 {
"EQUAL"
} else {
"WORSE"
};
Comparison {
status,
dveh: Some(dveh),
ddist_pct: Some(ddist_pct),
bks_text: format!("{}/{:.2}", bks.vehicles, bks.distance),
}
}
fn infer_group(instance_name: &str) -> String {
if let Some((left, _)) = instance_name.split_once('_') {
return left.to_ascii_uppercase();
}
let prefix_len = instance_name
.chars()
.take_while(char::is_ascii_alphabetic)
.count();
if prefix_len > 0 && prefix_len < instance_name.len() {
return instance_name[..prefix_len].to_ascii_uppercase();
}
instance_name.to_ascii_uppercase()
}
fn mean(xs: &[f64]) -> Option<f64> {
if xs.is_empty() {
None
} else {
Some(xs.iter().sum::<f64>() / xs.len() as f64)
}
}
fn median(mut xs: Vec<f64>) -> Option<f64> {
if xs.is_empty() {
return None;
}
xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = xs.len();
if n % 2 == 1 {
Some(xs[n / 2])
} else {
Some(f64::midpoint(xs[n / 2 - 1], xs[n / 2]))
}
}
fn print_table(results: &[RunResult], bks_map: &HashMap<String, BksRecord>) {
let headers = vec![
"Instance", "Group", "Time(s)", "Veh", "Dist", "BKS(V/D)", "dVeh", "dDist%", "Status",
];
let mut rows = Vec::<Vec<String>>::new();
let mut counters = BTreeMap::<&'static str, usize>::new();
counters.insert("BETTER", 0);
counters.insert("EQUAL", 0);
counters.insert("WORSE", 0);
counters.insert("NO_BKS", 0);
counters.insert("FAILED", 0);
let mut sorted = results.to_vec();
sorted.sort_by_key(|r| r.instance.to_ascii_lowercase());
let mut compared_dveh = Vec::<f64>::new();
let mut compared_ddist = Vec::<f64>::new();
for r in &sorted {
let cmp = compare_to_bks(r, bks_map.get(&r.instance.to_ascii_lowercase()));
if let Some(c) = counters.get_mut(cmp.status) {
*c += 1;
}
if let Some(v) = cmp.dveh {
compared_dveh.push(f64::from(v));
}
if let Some(v) = cmp.ddist_pct {
compared_ddist.push(v);
}
rows.push(vec![
r.instance.clone(),
infer_group(&r.instance),
format!("{:.1}", r.seconds),
r.vehicles
.map_or_else(|| "-".to_string(), |v| v.to_string()),
r.distance
.map_or_else(|| "-".to_string(), |v| format!("{v:.2}")),
cmp.bks_text,
cmp.dveh
.map_or_else(|| "-".to_string(), |v| format!("{v:+}")),
cmp.ddist_pct
.map_or_else(|| "-".to_string(), |v| format!("{v:+.2}%")),
cmp.status.to_string(),
]);
}
let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
for row in &rows {
for (i, cell) in row.iter().enumerate() {
widths[i] = widths[i].max(cell.len());
}
}
let fmt_row = |vals: &[String]| -> String {
vals.iter()
.enumerate()
.map(|(i, v)| format!("{v:<width$}", width = widths[i]))
.collect::<Vec<_>>()
.join(" | ")
};
println!();
println!(
"{}",
fmt_row(
&headers
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
)
);
println!(
"{}",
widths
.iter()
.map(|w| "-".repeat(*w))
.collect::<Vec<_>>()
.join("-+-")
);
for row in &rows {
println!("{}", fmt_row(row));
}
let ok_runs: Vec<&RunResult> = results.iter().filter(|r| r.error.is_none()).collect();
let times: Vec<f64> = ok_runs.iter().map(|r| r.seconds).collect();
let ges_inner: Vec<f64> = ok_runs
.iter()
.filter_map(|r| r.ges_inner_iterations.map(|v| v as f64))
.collect();
let ges_runs: Vec<f64> = ok_runs
.iter()
.filter_map(|r| r.ges_runs.map(|v| v as f64))
.collect();
let main_iters: Vec<f64> = ok_runs
.iter()
.filter_map(|r| r.main_iterations.map(|v| v as f64))
.collect();
println!();
println!(
"Summary: total={}, better={}, equal={}, worse={}, no_bks={}, failed={}",
results.len(),
counters["BETTER"],
counters["EQUAL"],
counters["WORSE"],
counters["NO_BKS"],
counters["FAILED"],
);
if !times.is_empty() {
let avg = mean(×).unwrap_or(0.0);
let med = median(times.clone()).unwrap_or(0.0);
let mx = times
.iter()
.copied()
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
println!("Runtime: avg={avg:.1}s, median={med:.1}s, max={mx:.1}s");
}
if !ges_inner.is_empty() {
let avg_ges_inner = mean(&ges_inner).unwrap_or(0.0);
let avg_ges_runs = mean(&ges_runs).unwrap_or(0.0);
let avg_main_iters = mean(&main_iters).unwrap_or(0.0);
let avg_ges_inner_per_sec = if times.is_empty() {
0.0
} else {
let denom = mean(×).unwrap_or(1.0);
if denom > 0.0 {
avg_ges_inner / denom
} else {
0.0
}
};
let avg_ges_inner_per_main_iter = if avg_main_iters > 0.0 {
avg_ges_inner / avg_main_iters
} else {
0.0
};
println!(
"GES iters: avg_inner={avg_ges_inner:.0}, avg_runs={avg_ges_runs:.1}, avg_inner/s={avg_ges_inner_per_sec:.1}, avg_inner/main_iter={avg_ges_inner_per_main_iter:.1}"
);
}
if !compared_dveh.is_empty() && !compared_ddist.is_empty() {
println!();
println!(
"Overall BKS aggregate: compared={}, avg_dVeh={:+.2}, avg_dDist={:+.2}%",
compared_dveh.len(),
mean(&compared_dveh).unwrap_or(0.0),
mean(&compared_ddist).unwrap_or(0.0)
);
}
let failed: Vec<&RunResult> = results.iter().filter(|r| r.error.is_some()).collect();
if !failed.is_empty() {
println!();
println!("Failures:");
for r in failed {
println!(
" - {}: {}",
r.instance,
r.error.as_deref().unwrap_or("unknown error")
);
}
}
}
fn main() -> std::process::ExitCode {
let args = match parse_args() {
Ok(ParseResult::Ok(args)) => args,
Ok(ParseResult::Help) => return std::process::ExitCode::SUCCESS,
Err(e) => {
eprintln!("[error] {e}");
return std::process::ExitCode::from(2);
}
};
let instances = match resolve_instances(&args.instances, &args.groups) {
Ok(v) => v,
Err(e) => {
eprintln!("[error] {e}");
return std::process::ExitCode::from(2);
}
};
if instances.is_empty() {
eprintln!("[error] No instances selected");
return std::process::ExitCode::from(2);
}
let bin_path = {
let p = PathBuf::from(&args.bin);
if p.is_absolute() {
p
} else {
repo_root().join(p)
}
};
if let Err(e) = ensure_binary(&bin_path, args.release) {
eprintln!("[error] {e}");
return std::process::ExitCode::from(2);
}
let bks_path = {
let p = PathBuf::from(&args.bks_file);
if p.is_absolute() {
p
} else {
repo_root().join(p)
}
};
let bks_map = match parse_bks(&bks_path) {
Ok(v) => v,
Err(e) => {
eprintln!("[error] {e}");
return std::process::ExitCode::from(2);
}
};
println!(
"Using BKS file: {} (records={})",
bks_path.display(),
bks_map.len()
);
let jobs = args.jobs.max(1).min(instances.len());
let seed_mode_text = match args.seed_mode {
SeedMode::Offset => "offset",
SeedMode::Fixed => "fixed",
};
println!(
"Running {} instances with jobs={}, time_limit={}s, base_seed={}, seed_mode={}, initial_method={}, GES_THREADS=1",
instances.len(),
jobs,
args.time_limit,
args.seed,
seed_mode_text,
args.initial_method
);
let tasks: Vec<(PathBuf, u64)> = instances
.iter()
.enumerate()
.map(|(idx, inst)| {
let seed = match args.seed_mode {
SeedMode::Fixed => args.seed,
SeedMode::Offset => args.seed + idx as u64,
};
(inst.clone(), seed)
})
.collect();
let total = tasks.len();
let tasks = Arc::new(tasks);
let next_idx = Arc::new(AtomicUsize::new(0));
let initial_method = Arc::new(args.initial_method.clone());
let (tx, rx) = mpsc::channel::<RunResult>();
let mut workers = Vec::with_capacity(jobs);
for _ in 0..jobs {
let tasks = Arc::clone(&tasks);
let next_idx = Arc::clone(&next_idx);
let tx = tx.clone();
let bin_path = bin_path.clone();
let time_limit = args.time_limit;
let initial_method = Arc::clone(&initial_method);
workers.push(thread::spawn(move || {
loop {
let idx = next_idx.fetch_add(1, Ordering::SeqCst);
if idx >= tasks.len() {
break;
}
let (instance, seed) = &tasks[idx];
let res = run_one(
&bin_path,
instance,
time_limit,
*seed,
initial_method.as_str(),
);
if tx.send(res).is_err() {
break;
}
}
}));
}
drop(tx);
let mut done = 0usize;
let mut results = Vec::with_capacity(total);
while done < total {
match rx.recv() {
Ok(r) => {
done += 1;
let flag = if r.error.is_none() { "ok" } else { "fail" };
let veh = r
.vehicles
.map_or_else(|| "-".to_string(), |v| v.to_string());
let dist = r
.distance
.map_or_else(|| "-".to_string(), |v| format!("{v:.2}"));
let ges_inner = r
.ges_inner_iterations
.map_or_else(|| "-".to_string(), |v| v.to_string());
println!(
"[{done:>3}/{total}] {inst:<15} {flag:<4} veh={veh} dist={dist} ges_inner={ges_inner} t={:.1}s",
r.seconds,
inst = r.instance
);
results.push(r);
}
Err(_) => break,
}
}
for worker in workers {
let _ = worker.join();
}
print_table(&results, &bks_map);
if results
.iter()
.all(|r| r.return_code == 0 && r.error.is_none())
{
std::process::ExitCode::SUCCESS
} else {
std::process::ExitCode::from(1)
}
}