use std::time::Instant;
use anyhow::{Result, bail};
use renkin::DEFAULT_BUILDING_BLOCKS;
use renkin::chem_env::{ChemEnv, default_rules, load_rules_from_file};
use renkin::search::{SearchConfig, find_routes};
use serde::Serialize;
#[derive(Serialize)]
struct BenchResult {
smiles: String,
name: String,
solved: bool,
routes_found: usize,
best_depth: Option<u32>,
time_ms: f64,
}
#[derive(Serialize)]
struct BenchReport {
total: usize,
solved: usize,
success_rate: f64,
avg_depth: f64,
avg_time_ms: f64,
results: Vec<BenchResult>,
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let mut input_path: Option<String> = None;
let mut bb_path: Option<String> = None;
let mut templates_path: Option<String> = None;
let mut max_depth: u32 = 5;
let mut beam_width: usize = 0;
let mut max_routes: usize = 1;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--input" | "-i" => {
i += 1;
if i < args.len() {
input_path = Some(args[i].clone());
}
}
"--depth" | "-d" => {
i += 1;
if i < args.len() {
max_depth = args[i].parse().unwrap_or(5);
}
}
"--beam-width" | "-w" => {
i += 1;
if i < args.len() {
beam_width = args[i].parse().unwrap_or(0);
}
}
"--max-routes" | "-n" => {
i += 1;
if i < args.len() {
max_routes = args[i].parse().unwrap_or(1);
}
}
"--building-blocks" | "-b" => {
i += 1;
if i < args.len() {
bb_path = Some(args[i].clone());
}
}
"--templates" => {
i += 1;
if i < args.len() {
templates_path = Some(args[i].clone());
}
}
_ => {}
}
i += 1;
}
let Some(input) = input_path else {
bail!(
"Usage: renkin-bench --input <smiles_file> [--depth <N>] \
[--beam-width <N>] [--building-blocks <path>] [--templates <path>]"
);
};
let content = std::fs::read_to_string(&input)?;
let targets: Vec<(String, String)> = content
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(|line| {
let mut parts = line.splitn(2, char::is_whitespace);
let smiles = parts.next().unwrap_or("").to_string();
let name = parts.next().unwrap_or("").trim().to_string();
(smiles, name)
})
.collect();
if targets.is_empty() {
bail!("No targets found in {input}");
}
let env = match bb_path {
Some(ref path) => ChemEnv::load(path)?,
None => ChemEnv::load("data/building_blocks.smi")
.unwrap_or_else(|_| ChemEnv::in_memory(DEFAULT_BUILDING_BLOCKS)),
};
let mut rules = default_rules();
if let Some(ref path) = templates_path {
let extra = load_rules_from_file(path);
eprintln!("Loaded {} templates from {path}", extra.len());
rules.extend(extra);
}
let config = SearchConfig {
max_depth,
max_routes,
beam_width,
};
eprintln!(
"Benchmarking {} targets (depth={}, beam_width={}) ...",
targets.len(),
max_depth,
beam_width
);
let mut results = Vec::new();
let mut total_depth_sum = 0u32;
let mut solved_count = 0usize;
for (smiles, name) in &targets {
let t0 = Instant::now();
let routes = find_routes(smiles, &env, &rules, &config).unwrap_or_default();
let elapsed_ms = t0.elapsed().as_secs_f64() * 1000.0;
let solved = !routes.is_empty();
let best_depth = routes.iter().map(|r| r.depth).min();
if solved {
solved_count += 1;
if let Some(d) = best_depth {
total_depth_sum += d;
}
}
eprintln!(
" [{}/{}] {} → {} route(s) in {:.1}ms",
results.len() + 1,
targets.len(),
smiles,
routes.len(),
elapsed_ms
);
results.push(BenchResult {
smiles: smiles.clone(),
name: name.clone(),
solved,
routes_found: routes.len(),
best_depth,
time_ms: elapsed_ms,
});
}
let total = results.len();
let success_rate = solved_count as f64 / total as f64;
let avg_depth = if solved_count > 0 {
total_depth_sum as f64 / solved_count as f64
} else {
0.0
};
let avg_time_ms = results.iter().map(|r| r.time_ms).sum::<f64>() / total as f64;
let report = BenchReport {
total,
solved: solved_count,
success_rate,
avg_depth,
avg_time_ms,
results,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}