Skip to main content

u_nesting_d3/
sa_packing.rs

1//! Simulated Annealing-based 3D bin packing optimization.
2//!
3//! This module provides Simulated Annealing based optimization for 3D bin packing
4//! problems. SA uses neighborhood operators to explore the solution space
5//! and accepts worse solutions with a probability that decreases over time.
6//!
7//! # Neighborhood Operators
8//!
9//! - **Swap**: Exchange positions of two items in the sequence
10//! - **Relocate**: Move an item to a different position
11//! - **Inversion**: Reverse a segment of the sequence
12//! - **Rotation**: Change the orientation of an item
13
14use crate::boundary::Boundary3D;
15use crate::geometry::Geometry3D;
16use crate::packing_utils::{
17    build_instances, build_unplaced_list, layer_place_items, packing_fitness, InstanceInfo,
18    PlacementItem,
19};
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::sync::Arc;
22use u_nesting_core::sa::{
23    NeighborhoodOperator, PermutationSolution, SaConfig, SaProblem, SaRunner, SaSolution,
24};
25use u_nesting_core::solver::Config;
26use u_nesting_core::SolveResult;
27
28/// SA problem definition for 3D bin packing.
29pub struct SaPackingProblem {
30    /// Input geometries.
31    geometries: Vec<Geometry3D>,
32    /// Boundary container.
33    boundary: Boundary3D,
34    /// Solver configuration.
35    config: Config,
36    /// Instance mapping.
37    instances: Vec<InstanceInfo>,
38    /// Maximum orientation count across all geometries.
39    max_orientation_count: usize,
40    /// Cancellation flag.
41    cancelled: Arc<AtomicBool>,
42}
43
44impl SaPackingProblem {
45    /// Creates a new SA packing problem.
46    pub fn new(
47        geometries: Vec<Geometry3D>,
48        boundary: Boundary3D,
49        config: Config,
50        cancelled: Arc<AtomicBool>,
51    ) -> Self {
52        let instances = build_instances(&geometries);
53        let max_orientation_count = instances
54            .iter()
55            .map(|i| i.orientation_count)
56            .max()
57            .unwrap_or(1);
58
59        Self {
60            geometries,
61            boundary,
62            config,
63            instances,
64            max_orientation_count,
65            cancelled,
66        }
67    }
68
69    /// Returns the total number of instances.
70    pub fn num_instances(&self) -> usize {
71        self.instances.len()
72    }
73
74    /// Decodes a solution into placements using layer-based packing.
75    pub fn decode(
76        &self,
77        solution: &PermutationSolution,
78    ) -> (Vec<u_nesting_core::Placement<f64>>, f64, usize) {
79        let n = self.instances.len();
80        if n == 0 || solution.sequence.is_empty() {
81            return (Vec::new(), 0.0, 0);
82        }
83
84        let items: Vec<PlacementItem> = solution
85            .sequence
86            .iter()
87            .enumerate()
88            .map(|(seq_idx, &instance_idx)| PlacementItem {
89                instance_idx,
90                orientation_idx: solution.rotations.get(seq_idx).copied().unwrap_or(0),
91            })
92            .collect();
93
94        let result = layer_place_items(
95            &items,
96            &self.instances,
97            &self.geometries,
98            &self.boundary,
99            &self.config,
100            &self.cancelled,
101        );
102        (result.placements, result.utilization, result.placed_count)
103    }
104}
105
106impl SaProblem for SaPackingProblem {
107    type Solution = PermutationSolution;
108
109    fn initial_solution<R: rand::Rng>(&self, rng: &mut R) -> Self::Solution {
110        PermutationSolution::random(self.instances.len(), self.max_orientation_count, rng)
111    }
112
113    fn neighbor<R: rand::Rng>(
114        &self,
115        solution: &Self::Solution,
116        operator: NeighborhoodOperator,
117        rng: &mut R,
118    ) -> Self::Solution {
119        match operator {
120            NeighborhoodOperator::Swap => solution.apply_swap(rng),
121            NeighborhoodOperator::Relocate => solution.apply_relocate(rng),
122            NeighborhoodOperator::Inversion => solution.apply_inversion(rng),
123            NeighborhoodOperator::Rotation => solution.apply_rotation(rng),
124            NeighborhoodOperator::Chain => solution.apply_chain(rng),
125        }
126    }
127
128    fn evaluate(&self, solution: &mut Self::Solution) {
129        let (_, utilization, placed_count) = self.decode(solution);
130        let fitness = packing_fitness(placed_count, self.instances.len(), utilization);
131        solution.set_objective(fitness);
132    }
133
134    fn available_operators(&self) -> Vec<NeighborhoodOperator> {
135        if self.max_orientation_count > 1 {
136            vec![
137                NeighborhoodOperator::Swap,
138                NeighborhoodOperator::Relocate,
139                NeighborhoodOperator::Inversion,
140                NeighborhoodOperator::Rotation,
141                NeighborhoodOperator::Chain,
142            ]
143        } else {
144            vec![
145                NeighborhoodOperator::Swap,
146                NeighborhoodOperator::Relocate,
147                NeighborhoodOperator::Inversion,
148                NeighborhoodOperator::Chain,
149            ]
150        }
151    }
152
153    fn on_temperature_change(
154        &self,
155        temperature: f64,
156        iteration: u64,
157        best: &Self::Solution,
158        _current: &Self::Solution,
159    ) {
160        log::debug!(
161            "SA 3D Packing Iteration {}: temp={:.4}, best_fitness={:.4}",
162            iteration,
163            temperature,
164            best.objective()
165        );
166    }
167}
168
169/// Runs SA-based 3D bin packing optimization.
170pub fn run_sa_packing(
171    geometries: &[Geometry3D],
172    boundary: &Boundary3D,
173    config: &Config,
174    sa_config: SaConfig,
175    cancelled: Arc<AtomicBool>,
176) -> SolveResult<f64> {
177    let problem = SaPackingProblem::new(
178        geometries.to_vec(),
179        boundary.clone(),
180        config.clone(),
181        cancelled.clone(),
182    );
183
184    let runner = SaRunner::new(sa_config, problem);
185
186    // Set cancellation
187    let cancel_handle = runner.cancel_handle();
188    let cancelled_clone = cancelled.clone();
189    std::thread::spawn(move || {
190        while !cancelled_clone.load(Ordering::Relaxed) {
191            std::thread::sleep(std::time::Duration::from_millis(100));
192        }
193        cancel_handle.store(true, Ordering::Relaxed);
194    });
195
196    let sa_result = runner.run();
197
198    // Decode the best solution
199    let problem = SaPackingProblem::new(
200        geometries.to_vec(),
201        boundary.clone(),
202        config.clone(),
203        Arc::new(AtomicBool::new(false)),
204    );
205
206    let (placements, utilization, _placed_count) = problem.decode(&sa_result.best);
207    let unplaced = build_unplaced_list(&placements, geometries);
208
209    let mut result = SolveResult::new();
210    result.placements = placements;
211    result.unplaced = unplaced;
212    result.boundaries_used = 1;
213    result.utilization = utilization;
214    result.computation_time_ms = sa_result.elapsed.as_millis() as u64;
215    result.iterations = Some(sa_result.iterations);
216    result.best_fitness = Some(sa_result.best.objective());
217    result.fitness_history = Some(sa_result.history);
218    result.strategy = Some("SimulatedAnnealing".to_string());
219    result.cancelled = cancelled.load(Ordering::Relaxed);
220    result.target_reached = sa_result.target_reached;
221
222    result
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_sa_packing_basic() {
231        let geometries = vec![
232            Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
233            Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
234        ];
235
236        let boundary = Boundary3D::new(100.0, 80.0, 50.0);
237        let config = Config::default();
238        let sa_config = SaConfig::default()
239            .with_initial_temp(100.0)
240            .with_final_temp(0.1)
241            .with_cooling_rate(0.9)
242            .with_iterations_per_temp(20)
243            .with_max_iterations(500);
244
245        let result = run_sa_packing(
246            &geometries,
247            &boundary,
248            &config,
249            sa_config,
250            Arc::new(AtomicBool::new(false)),
251        );
252
253        assert!(result.utilization > 0.0);
254        assert!(!result.placements.is_empty());
255        assert_eq!(result.strategy, Some("SimulatedAnnealing".to_string()));
256    }
257
258    #[test]
259    fn test_sa_packing_all_placed() {
260        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(4)];
261
262        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
263        let config = Config::default();
264        let sa_config = SaConfig::default()
265            .with_initial_temp(100.0)
266            .with_final_temp(0.1)
267            .with_max_iterations(1000);
268
269        let result = run_sa_packing(
270            &geometries,
271            &boundary,
272            &config,
273            sa_config,
274            Arc::new(AtomicBool::new(false)),
275        );
276
277        // All 4 boxes should fit easily
278        assert_eq!(result.placements.len(), 4);
279        assert!(result.unplaced.is_empty());
280    }
281
282    #[test]
283    fn test_sa_packing_with_orientations() {
284        use crate::geometry::OrientationConstraint;
285
286        // Long boxes that benefit from rotation
287        let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
288            .with_quantity(3)
289            .with_orientation(OrientationConstraint::Any)];
290
291        let boundary = Boundary3D::new(60.0, 60.0, 60.0);
292        let config = Config::default();
293        let sa_config = SaConfig::default()
294            .with_initial_temp(100.0)
295            .with_final_temp(0.1)
296            .with_max_iterations(500);
297
298        let result = run_sa_packing(
299            &geometries,
300            &boundary,
301            &config,
302            sa_config,
303            Arc::new(AtomicBool::new(false)),
304        );
305
306        assert!(result.utilization > 0.0);
307        assert!(!result.placements.is_empty());
308    }
309
310    #[test]
311    fn test_sa_problem_decode() {
312        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2)];
313
314        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
315        let config = Config::default();
316        let cancelled = Arc::new(AtomicBool::new(false));
317
318        let problem = SaPackingProblem::new(geometries, boundary, config, cancelled);
319
320        assert_eq!(problem.num_instances(), 2);
321
322        // Create a random solution and decode
323        let mut rng = rand::rng();
324        let solution = PermutationSolution::random(problem.num_instances(), 1, &mut rng);
325        let (placements, utilization, placed_count) = problem.decode(&solution);
326
327        // Should place at least one item
328        assert!(placed_count >= 1);
329        assert_eq!(placements.len(), placed_count);
330        if placed_count > 0 {
331            assert!(utilization > 0.0);
332        }
333    }
334
335    #[test]
336    fn test_sa_packing_mass_constraint() {
337        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0)
338            .with_quantity(10)
339            .with_mass(100.0)];
340
341        let boundary = Boundary3D::new(100.0, 100.0, 100.0).with_max_mass(350.0);
342        let config = Config::default();
343        let sa_config = SaConfig::default()
344            .with_initial_temp(100.0)
345            .with_final_temp(0.1)
346            .with_max_iterations(500);
347
348        let result = run_sa_packing(
349            &geometries,
350            &boundary,
351            &config,
352            sa_config,
353            Arc::new(AtomicBool::new(false)),
354        );
355
356        // Should only place 3 boxes due to 350 mass limit
357        assert!(result.placements.len() <= 3);
358    }
359}