use super::model::{ThreeDOptions, ThreeDProblem, ThreeDSolution};
use crate::Result;
fn run_candidates(
candidates: &[SolverFn],
problem: &ThreeDProblem,
options: &ThreeDOptions,
) -> Vec<Result<ThreeDSolution>> {
#[cfg(feature = "parallel")]
if crate::parallel::use_parallel() {
use rayon::prelude::*;
return candidates.par_iter().map(|solver| solver(problem, options)).collect();
}
candidates.iter().map(|solver| solver(problem, options)).collect()
}
pub(super) fn solve_auto(
problem: &ThreeDProblem,
options: &ThreeDOptions,
) -> Result<ThreeDSolution> {
if options.guillotine_required {
return solve_auto_guillotine(problem, options);
}
let mut all_candidates: Vec<SolverFn> = vec![
super::extreme_points::solve_extreme_points,
super::extreme_points::solve_extreme_points_residual_space,
super::extreme_points::solve_extreme_points_contact_point,
super::guillotine::solve_guillotine_3d,
super::layer::solve_layer_building,
super::sorted::solve_first_fit_decreasing_volume,
];
if options.multistart_runs > 0 {
all_candidates.push(super::multi_start::solve_multi_start);
all_candidates.push(super::local_search::solve_local_search);
}
let results = run_candidates(&all_candidates, problem, options);
let total = results.len();
let mut best: Option<ThreeDSolution> = None;
let mut last_error: Option<crate::BinPackingError> = None;
for result in results {
match result {
Ok(sol) => {
let is_better = best.as_ref().is_none_or(|b| sol.is_better_than(b));
if is_better {
best = Some(sol);
}
}
Err(e) => {
last_error = Some(e);
}
}
}
match best {
Some(mut sol) => {
let winning_alg = sol.algorithm.clone();
sol.metrics
.notes
.push(format!("auto: tried {total} algorithms, best was {winning_alg}"));
Ok(sol)
}
None => Err(last_error.unwrap_or_else(|| {
crate::BinPackingError::Unsupported(
"auto: no candidate algorithms were run".to_string(),
)
})),
}
}
type SolverFn = fn(&ThreeDProblem, &ThreeDOptions) -> Result<ThreeDSolution>;
fn solve_auto_guillotine(
problem: &ThreeDProblem,
options: &ThreeDOptions,
) -> Result<ThreeDSolution> {
let candidates: Vec<SolverFn> = vec![
super::guillotine::solve_guillotine_3d,
super::guillotine::solve_guillotine_3d_best_short_side_fit,
super::guillotine::solve_guillotine_3d_best_long_side_fit,
super::guillotine::solve_guillotine_3d_shorter_leftover_axis,
super::guillotine::solve_guillotine_3d_longer_leftover_axis,
super::guillotine::solve_guillotine_3d_min_volume_split,
super::guillotine::solve_guillotine_3d_max_volume_split,
super::layer::solve_layer_building_guillotine,
];
let total = candidates.len();
let results = run_candidates(&candidates, problem, options);
let mut best: Option<ThreeDSolution> = None;
let mut last_error: Option<crate::BinPackingError> = None;
for result in results {
match result {
Ok(sol) if sol.guillotine => {
if best.as_ref().is_none_or(|current| sol.is_better_than(current)) {
best = Some(sol);
}
}
Ok(_sol) => {}
Err(err) => last_error = Some(err),
}
}
match best {
Some(mut sol) => {
let winning_alg = sol.algorithm.clone();
sol.metrics.notes.push(format!(
"auto: tried {total} guillotine algorithms, best was {winning_alg}",
));
Ok(sol)
}
None => Err(last_error.unwrap_or_else(|| {
crate::BinPackingError::Unsupported(
"auto: no guillotine-compatible candidate succeeded".to_string(),
)
})),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::three_d::model::{Bin3D, BoxDemand3D, RotationMask3D, ThreeDProblem};
fn simple_problem() -> ThreeDProblem {
ThreeDProblem {
bins: vec![Bin3D {
name: "bin".to_string(),
width: 10,
height: 10,
depth: 10,
cost: 1.0,
quantity: None,
}],
demands: vec![BoxDemand3D {
name: "box".to_string(),
width: 5,
height: 5,
depth: 5,
quantity: 1,
allowed_rotations: RotationMask3D::ALL,
}],
}
}
#[test]
fn auto_does_not_error_on_simple_problem() {
let problem = simple_problem();
let options = ThreeDOptions::default();
let result = solve_auto(&problem, &options);
assert!(result.is_ok(), "expected Ok, got {result:?}");
}
#[test]
fn auto_preserves_winning_leaf_algorithm() {
let problem = simple_problem();
let options = ThreeDOptions::default();
let solution = solve_auto(&problem, &options).expect("solve_auto should succeed");
assert_ne!(solution.algorithm, "auto");
assert!(!solution.algorithm.is_empty());
}
#[test]
fn auto_adds_diagnostic_note() {
let problem = simple_problem();
let options = ThreeDOptions::default();
let solution = solve_auto(&problem, &options).expect("solve_auto should succeed");
let has_note = solution.metrics.notes.iter().any(|n| n.starts_with("auto: tried "));
assert!(has_note, "expected diagnostic note, got {:?}", solution.metrics.notes);
}
#[test]
fn auto_guillotine_required_returns_guillotine_solution() {
let problem = simple_problem();
let options = ThreeDOptions { guillotine_required: true, ..ThreeDOptions::default() };
let solution = solve_auto(&problem, &options).expect("solve_auto should succeed");
assert!(solution.guillotine);
assert_ne!(solution.algorithm, "auto");
}
#[test]
fn auto_with_multistart_runs_tier2_candidates() {
let problem = ThreeDProblem {
bins: vec![Bin3D {
name: "bin".to_string(),
width: 10,
height: 10,
depth: 10,
cost: 1.0,
quantity: None,
}],
demands: vec![
BoxDemand3D {
name: "a".to_string(),
width: 5,
height: 5,
depth: 5,
quantity: 2,
allowed_rotations: RotationMask3D::ALL,
},
BoxDemand3D {
name: "b".to_string(),
width: 3,
height: 3,
depth: 3,
quantity: 4,
allowed_rotations: RotationMask3D::ALL,
},
],
};
let options =
ThreeDOptions { multistart_runs: 4, seed: Some(42), ..ThreeDOptions::default() };
let solution = solve_auto(&problem, &options).expect("auto with multistart");
assert!(solution.unplaced.is_empty());
assert!(solution.metrics.notes.iter().any(|n| n.starts_with("auto: tried ")));
}
#[test]
fn auto_is_deterministic() {
let problem = simple_problem();
let options =
ThreeDOptions { multistart_runs: 2, seed: Some(7), ..ThreeDOptions::default() };
let first = solve_auto(&problem, &options).expect("first");
let second = solve_auto(&problem, &options).expect("second");
assert_eq!(first.bin_count, second.bin_count);
assert_eq!(first.algorithm, second.algorithm);
assert_eq!(first.total_waste_volume, second.total_waste_volume);
assert_eq!(first.total_cost, second.total_cost);
assert_eq!(first.unplaced.len(), second.unplaced.len());
}
}