use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::time::Instant;
use anyhow::{Context as _, Result, anyhow, bail};
use bywind::{
BAKE_STEP, MapBounds, SavedSolution, SegmentMetrics,
fmt::{format_duration_breakdown, format_fuel_auto, format_land_km},
gbest_segment_metrics,
};
use crate::display::print_segment_table;
use crate::error::AppError;
pub fn run(solution_path: &Path, map_path: Option<&Path>) -> Result<(), AppError> {
let saved = read_solution(solution_path)?;
print_solution_metadata(solution_path, &saved);
if let Some(map_path) = map_path {
let segment_stats = score_against_map(&saved, map_path)?;
print_route_summary(&saved, &segment_stats);
} else {
eprintln!();
eprintln!("(pass --map <wind_map> for per-segment fuel / time / speed / land totals)");
}
Ok(())
}
fn read_solution(path: &Path) -> Result<SavedSolution> {
let file = File::open(path).with_context(|| format!("opening {}", path.display()))?;
let reader = BufReader::new(file);
serde_json::from_reader(reader)
.with_context(|| format!("parsing SavedSolution from {}", path.display()))
}
fn score_against_map(
saved: &SavedSolution,
map_path: &Path,
) -> Result<Vec<SegmentMetrics>, AppError> {
eprintln!();
eprintln!("loading {}...", map_path.display());
let load_start = Instant::now();
let map = bywind::io::load(map_path, 1, None)
.with_context(|| format!("loading wind map from {}", map_path.display()))?;
eprintln!(
" loaded in {:.2}s: {} frame(s), step = {} s",
load_start.elapsed().as_secs_f64(),
map.frame_count(),
map.step_seconds(),
);
let map_bounds = MapBounds::from_wind_map(&map).ok_or_else(|| {
AppError::no_result(anyhow!("wind map {} has no rows", map_path.display()))
})?;
if !map_bounds.is_non_degenerate() {
return Err(AppError::no_result(anyhow!(
"wind map {} has a degenerate bbox; can't bake for re-scoring",
map_path.display(),
)));
}
let (_wc, route_evolution) = saved.to_route_evolution().map_err(anyhow::Error::from)?;
let (start_xy, end_xy) = endpoints_from_saved(saved)?;
let route_bounds = map_bounds.to_route_bounds(start_xy, end_xy);
let bake_bounds = map_bounds.to_bake_bounds(BAKE_STEP);
eprintln!("baking wind map for re-scoring...");
let bake_start = Instant::now();
let baked = map.bake(bake_bounds);
eprintln!(" baked in {:.2}s", bake_start.elapsed().as_secs_f64());
let boat = bywind::BoatConfig::default().to_boat();
let metrics = gbest_segment_metrics(
&route_evolution,
0,
&boat,
&baked,
route_bounds.step_distance_max,
)
.ok_or_else(|| AppError::internal(anyhow!("rebuilt route evolution has no iterations")))?;
Ok(metrics)
}
fn endpoints_from_saved(saved: &SavedSolution) -> Result<((f64, f64), (f64, f64))> {
let n = saved.n;
if saved.xs.len() != n || saved.ys.len() != n || n < 2 {
bail!(
"saved solution has unexpected shape: n={n}, xs={}, ys={}",
saved.xs.len(),
saved.ys.len(),
);
}
let (Some(&start_x), Some(&start_y), Some(&end_x), Some(&end_y)) = (
saved.xs.first(),
saved.ys.first(),
saved.xs.last(),
saved.ys.last(),
) else {
bail!("saved solution has empty coordinate arrays");
};
Ok(((start_x, start_y), (end_x, end_y)))
}
fn print_solution_metadata(path: &Path, saved: &SavedSolution) {
eprintln!("=== Saved solution ===");
eprintln!("Path: {}", path.display());
eprintln!("Waypoints: {}", saved.n);
eprintln!("Fitness: {:.4}", saved.best_fit);
eprintln!();
eprintln!("Search params used to produce it:");
eprintln!(" time_weight: {}", saved.time_weight);
eprintln!(" fuel_weight: {}", saved.fuel_weight);
eprintln!(" particles_space: {}", saved.particles_space);
eprintln!(" particles_time: {}", saved.particles_time);
eprintln!(" iter_space: {}", saved.iter_space);
eprintln!(" iter_time: {}", saved.iter_time);
eprintln!(" topology: {}", saved.topology);
if let Some(seed) = saved.seed {
eprintln!(" seed: {seed}");
}
eprintln!(
" path_kick_probability: {}",
saved.path_kick_probability
);
eprintln!(
" path_kick_gamma_0_fraction: {}",
saved.path_kick_gamma_0_fraction
);
eprintln!(
" path_kick_gamma_min_fraction: {}",
saved.path_kick_gamma_min_fraction
);
if let (Some(&start_x), Some(&start_y), Some(&end_x), Some(&end_y)) = (
saved.xs.first(),
saved.ys.first(),
saved.xs.last(),
saved.ys.last(),
) {
eprintln!();
eprintln!("Route endpoints: ({start_x:.3}, {start_y:.3}) -> ({end_x:.3}, {end_y:.3})",);
}
}
fn print_route_summary(saved: &SavedSolution, segment_stats: &[SegmentMetrics]) {
let total_time: f64 = segment_stats.iter().map(|m| m.time).sum();
let total_fuel: f64 = segment_stats.iter().map(|m| m.fuel).sum();
let total_land_metres: f64 = segment_stats.iter().map(|m| m.land_metres).sum();
eprintln!();
eprintln!("=== Route (re-scored against --map) ===");
eprintln!("Total time: {}", format_duration_breakdown(total_time));
eprintln!("Total fuel: {}", format_fuel_auto(total_fuel));
eprintln!("Total land: {}", format_land_km(total_land_metres));
eprintln!(
"Fitness: {:.4} (from JSON; not recomputed)",
saved.best_fit
);
eprintln!();
eprintln!("=== Segments ===");
print_segment_table(segment_stats);
}