use rand::{Rng, SeedableRng, rngs::SmallRng};
use super::common::volume_u64;
use super::extreme_points::{ExtremePointsScoring, solve_with_scoring_on_items};
use super::local_search::improve;
use super::model::{ItemInstance3D, ThreeDOptions, ThreeDProblem, ThreeDSolution};
use crate::Result;
const GRASP_ALPHA: f64 = 0.3;
const DEFAULT_SEED: u64 = 0;
fn rcl_order(mut items: Vec<ItemInstance3D>, rng: &mut SmallRng) -> Vec<ItemInstance3D> {
let mut ordered = Vec::with_capacity(items.len());
while !items.is_empty() {
let volumes: Vec<u64> =
items.iter().map(|it| volume_u64(it.width, it.height, it.depth)).collect();
let max_vol = *volumes.iter().max().unwrap_or(&0);
let threshold = (max_vol as f64 * (1.0 - GRASP_ALPHA)) as u64;
let mut rcl: Vec<usize> = volumes
.iter()
.enumerate()
.filter_map(|(i, &v)| if v >= threshold { Some(i) } else { None })
.collect();
if rcl.is_empty()
&& let Some((best_idx, _)) = volumes.iter().enumerate().max_by_key(|&(_, &v)| v)
{
rcl.push(best_idx);
}
if rcl.is_empty() {
break;
}
let chosen_idx = rcl[rng.random_range(0..rcl.len())];
ordered.push(items.swap_remove(chosen_idx));
}
ordered
}
pub(super) fn solve_grasp(
problem: &ThreeDProblem,
options: &ThreeDOptions,
) -> Result<ThreeDSolution> {
let base_items = problem.expanded_items();
let base_seed = options.seed.unwrap_or(DEFAULT_SEED);
let runs = options.multistart_runs.max(1);
let iteration_results = crate::parallel::par_map_indexed(runs, |run| {
let mut rng = SmallRng::seed_from_u64(crate::parallel::iteration_seed(base_seed, run));
let ordered_items = rcl_order(base_items.clone(), &mut rng);
let construction_result = solve_with_scoring_on_items(
problem,
ordered_items,
options,
ExtremePointsScoring::VolumeFitResidual,
"grasp",
);
match construction_result {
Ok(constructed) => {
let mut candidate = improve(constructed, problem, options, &mut rng);
candidate.algorithm = "grasp".to_string();
Ok((run, candidate))
}
Err(err) => Err((run, err)),
}
});
let mut best: Option<ThreeDSolution> = None;
let mut pending_notes: Vec<String> = Vec::new();
let mut first_error: Option<crate::BinPackingError> = None;
for result in iteration_results {
match result {
Ok((_run, candidate)) => {
best = Some(match best.take() {
Some(current) if current.is_better_than(&candidate) => current,
_ => candidate,
});
}
Err((run, err)) => {
pending_notes.push(format!("grasp: run {run} construction failed: {err}"));
if first_error.is_none() {
first_error = Some(err);
}
}
}
}
match best {
Some(mut solution) => {
solution.algorithm = "grasp".to_string();
solution.metrics.iterations = runs;
solution.metrics.notes.extend(pending_notes);
solution.metrics.notes.push(format!("grasp: {runs} iteration(s)"));
Ok(solution)
}
None => Err(first_error.unwrap_or_else(|| {
crate::BinPackingError::Unsupported("grasp: zero iterations executed".to_string())
})),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::three_d::model::{Bin3D, BoxDemand3D, RotationMask3D, ThreeDAlgorithm};
fn bin(w: u32, h: u32, d: u32) -> Bin3D {
Bin3D { name: "b".into(), width: w, height: h, depth: d, cost: 1.0, quantity: None }
}
fn demand(name: &str, w: u32, h: u32, d: u32, qty: usize) -> BoxDemand3D {
BoxDemand3D {
name: name.into(),
width: w,
height: h,
depth: d,
quantity: qty,
allowed_rotations: RotationMask3D::ALL,
}
}
fn options_with(seed: u64, runs: usize) -> ThreeDOptions {
ThreeDOptions {
algorithm: ThreeDAlgorithm::Grasp,
multistart_runs: runs,
seed: Some(seed),
..ThreeDOptions::default()
}
}
#[test]
fn grasp_places_all_items_two_item_problem() {
let problem = ThreeDProblem {
bins: vec![bin(10, 10, 10)],
demands: vec![demand("a", 4, 4, 4, 1), demand("b", 3, 3, 3, 1)],
};
let options = options_with(1, 1);
let solution = solve_grasp(&problem, &options).expect("solve");
assert!(solution.unplaced.is_empty(), "expected no unplaced items");
assert_eq!(solution.bin_count, 1);
}
#[test]
fn grasp_multiple_iterations_no_panic() {
let problem = ThreeDProblem {
bins: vec![bin(10, 10, 10)],
demands: vec![demand("x", 5, 5, 5, 2), demand("y", 3, 3, 3, 3)],
};
let options = options_with(42, 3);
let solution = solve_grasp(&problem, &options).expect("solve");
assert!(solution.bin_count >= 1);
assert_eq!(solution.metrics.iterations, 3);
assert!(!solution.metrics.notes.is_empty());
}
#[test]
fn grasp_deterministic_under_same_seed() {
let problem = ThreeDProblem {
bins: vec![bin(10, 10, 10)],
demands: vec![
demand("big", 5, 5, 5, 2),
demand("mid", 3, 4, 5, 4),
demand("tiny", 2, 2, 2, 6),
],
};
let options = options_with(0xDEAD_BEEF, 5);
let first = solve_grasp(&problem, &options).expect("first run");
let second = solve_grasp(&problem, &options).expect("second run");
assert_eq!(first.bin_count, second.bin_count, "same seed must produce the same bin count");
}
}