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 std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::Arc;
18use u_nesting_core::geometry::{Boundary, Geometry};
19use u_nesting_core::sa::{
20    NeighborhoodOperator, PermutationSolution, SaConfig, SaProblem, SaRunner, SaSolution,
21};
22use u_nesting_core::solver::Config;
23use u_nesting_core::{Placement, SolveResult};
24
25/// Instance information for decoding.
26#[derive(Debug, Clone)]
27struct InstanceInfo {
28    /// Index into the geometries array.
29    geometry_idx: usize,
30    /// Instance number within this geometry's quantity.
31    instance_num: usize,
32    /// Number of allowed orientations.
33    orientation_count: usize,
34}
35
36/// SA problem definition for 3D bin packing.
37pub struct SaPackingProblem {
38    /// Input geometries.
39    geometries: Vec<Geometry3D>,
40    /// Boundary container.
41    boundary: Boundary3D,
42    /// Solver configuration.
43    config: Config,
44    /// Instance mapping.
45    instances: Vec<InstanceInfo>,
46    /// Maximum orientation count across all geometries.
47    max_orientation_count: usize,
48    /// Cancellation flag.
49    cancelled: Arc<AtomicBool>,
50}
51
52impl SaPackingProblem {
53    /// Creates a new SA packing problem.
54    pub fn new(
55        geometries: Vec<Geometry3D>,
56        boundary: Boundary3D,
57        config: Config,
58        cancelled: Arc<AtomicBool>,
59    ) -> Self {
60        // Build instance mapping
61        let mut instances = Vec::new();
62        let mut max_orientation_count = 1;
63
64        for (geom_idx, geom) in geometries.iter().enumerate() {
65            let orient_count = geom.allowed_orientations().len();
66            max_orientation_count = max_orientation_count.max(orient_count);
67
68            for instance_num in 0..geom.quantity() {
69                instances.push(InstanceInfo {
70                    geometry_idx: geom_idx,
71                    instance_num,
72                    orientation_count: orient_count,
73                });
74            }
75        }
76
77        Self {
78            geometries,
79            boundary,
80            config,
81            instances,
82            max_orientation_count,
83            cancelled,
84        }
85    }
86
87    /// Returns the total number of instances.
88    pub fn num_instances(&self) -> usize {
89        self.instances.len()
90    }
91
92    /// Decodes a solution into placements using layer-based packing.
93    pub fn decode(&self, solution: &PermutationSolution) -> (Vec<Placement<f64>>, f64, usize) {
94        let n = self.instances.len();
95        if n == 0 || solution.sequence.is_empty() {
96            return (Vec::new(), 0.0, 0);
97        }
98
99        let mut placements = Vec::new();
100
101        let margin = self.config.margin;
102        let spacing = self.config.spacing;
103
104        let bound_max_x = self.boundary.width() - margin;
105        let bound_max_y = self.boundary.depth() - margin;
106        let bound_max_z = self.boundary.height() - margin;
107
108        // Track current position in layer-based packing
109        let mut current_x = margin;
110        let mut current_y = margin;
111        let mut current_z = margin;
112        let mut row_depth = 0.0_f64;
113        let mut layer_height = 0.0_f64;
114
115        let mut total_placed_volume = 0.0;
116        let mut total_placed_mass = 0.0;
117        let mut placed_count = 0;
118
119        // Place items in the solution order
120        for (seq_idx, &instance_idx) in solution.sequence.iter().enumerate() {
121            if self.cancelled.load(Ordering::Relaxed) {
122                break;
123            }
124
125            if instance_idx >= self.instances.len() {
126                continue;
127            }
128
129            let info = &self.instances[instance_idx];
130            let geom = &self.geometries[info.geometry_idx];
131
132            // Get orientation from solution
133            let orientation_idx = solution.rotations.get(seq_idx).copied().unwrap_or(0);
134            let orientation_idx = orientation_idx % info.orientation_count.max(1);
135
136            // Get dimensions for this orientation
137            let dims = geom.dimensions_for_orientation(orientation_idx);
138            let g_width = dims.x;
139            let g_depth = dims.y;
140            let g_height = dims.z;
141
142            // Check mass constraint
143            if let (Some(max_mass), Some(item_mass)) = (self.boundary.max_mass(), geom.mass()) {
144                if total_placed_mass + item_mass > max_mass {
145                    continue;
146                }
147            }
148
149            // Try to fit in current row
150            if current_x + g_width > bound_max_x {
151                // Move to next row
152                current_x = margin;
153                current_y += row_depth + spacing;
154                row_depth = 0.0;
155            }
156
157            // Check if fits in current layer (y direction)
158            if current_y + g_depth > bound_max_y {
159                // Move to next layer
160                current_x = margin;
161                current_y = margin;
162                current_z += layer_height + spacing;
163                row_depth = 0.0;
164                layer_height = 0.0;
165            }
166
167            // Check if fits in container height
168            if current_z + g_height > bound_max_z {
169                continue;
170            }
171
172            // Place the item
173            let placement = Placement::new_3d(
174                geom.id().clone(),
175                info.instance_num,
176                current_x,
177                current_y,
178                current_z,
179                0.0,
180                0.0,
181                0.0, // Orientation is encoded in orientation_idx, not Euler angles
182            );
183
184            placements.push(placement);
185            total_placed_volume += geom.measure();
186            if let Some(mass) = geom.mass() {
187                total_placed_mass += mass;
188            }
189            placed_count += 1;
190
191            // Update position for next item
192            current_x += g_width + spacing;
193            row_depth = row_depth.max(g_depth);
194            layer_height = layer_height.max(g_height);
195        }
196
197        let utilization = total_placed_volume / self.boundary.measure();
198        (placements, utilization, placed_count)
199    }
200}
201
202impl SaProblem for SaPackingProblem {
203    type Solution = PermutationSolution;
204
205    fn initial_solution<R: rand::Rng>(&self, rng: &mut R) -> Self::Solution {
206        PermutationSolution::random(self.instances.len(), self.max_orientation_count, rng)
207    }
208
209    fn neighbor<R: rand::Rng>(
210        &self,
211        solution: &Self::Solution,
212        operator: NeighborhoodOperator,
213        rng: &mut R,
214    ) -> Self::Solution {
215        match operator {
216            NeighborhoodOperator::Swap => solution.apply_swap(rng),
217            NeighborhoodOperator::Relocate => solution.apply_relocate(rng),
218            NeighborhoodOperator::Inversion => solution.apply_inversion(rng),
219            NeighborhoodOperator::Rotation => solution.apply_rotation(rng),
220            NeighborhoodOperator::Chain => solution.apply_chain(rng),
221        }
222    }
223
224    fn evaluate(&self, solution: &mut Self::Solution) {
225        let total_instances = self.instances.len();
226        let (_, utilization, placed_count) = self.decode(solution);
227
228        // Fitness = placement ratio * 100 + utilization * 10
229        let placement_ratio = placed_count as f64 / total_instances.max(1) as f64;
230        let fitness = placement_ratio * 100.0 + utilization * 10.0;
231
232        solution.set_objective(fitness);
233    }
234
235    fn available_operators(&self) -> Vec<NeighborhoodOperator> {
236        if self.max_orientation_count > 1 {
237            vec![
238                NeighborhoodOperator::Swap,
239                NeighborhoodOperator::Relocate,
240                NeighborhoodOperator::Inversion,
241                NeighborhoodOperator::Rotation,
242                NeighborhoodOperator::Chain,
243            ]
244        } else {
245            vec![
246                NeighborhoodOperator::Swap,
247                NeighborhoodOperator::Relocate,
248                NeighborhoodOperator::Inversion,
249                NeighborhoodOperator::Chain,
250            ]
251        }
252    }
253
254    fn on_temperature_change(
255        &self,
256        temperature: f64,
257        iteration: u64,
258        best: &Self::Solution,
259        _current: &Self::Solution,
260    ) {
261        log::debug!(
262            "SA 3D Packing Iteration {}: temp={:.4}, best_fitness={:.4}",
263            iteration,
264            temperature,
265            best.objective()
266        );
267    }
268}
269
270/// Runs SA-based 3D bin packing optimization.
271pub fn run_sa_packing(
272    geometries: &[Geometry3D],
273    boundary: &Boundary3D,
274    config: &Config,
275    sa_config: SaConfig,
276    cancelled: Arc<AtomicBool>,
277) -> SolveResult<f64> {
278    let problem = SaPackingProblem::new(
279        geometries.to_vec(),
280        boundary.clone(),
281        config.clone(),
282        cancelled.clone(),
283    );
284
285    let runner = SaRunner::new(sa_config, problem);
286
287    // Set cancellation
288    let cancel_handle = runner.cancel_handle();
289    let cancelled_clone = cancelled.clone();
290    std::thread::spawn(move || {
291        while !cancelled_clone.load(Ordering::Relaxed) {
292            std::thread::sleep(std::time::Duration::from_millis(100));
293        }
294        cancel_handle.store(true, Ordering::Relaxed);
295    });
296
297    let sa_result = runner.run();
298
299    // Decode the best solution
300    let problem = SaPackingProblem::new(
301        geometries.to_vec(),
302        boundary.clone(),
303        config.clone(),
304        Arc::new(AtomicBool::new(false)),
305    );
306
307    let (placements, utilization, _placed_count) = problem.decode(&sa_result.best);
308
309    // Build unplaced list
310    let mut unplaced = Vec::new();
311    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
312    for p in &placements {
313        placed_ids.insert(p.geometry_id.clone());
314    }
315    for geom in geometries {
316        if !placed_ids.contains(geom.id()) {
317            unplaced.push(geom.id().clone());
318        }
319    }
320
321    let mut result = SolveResult::new();
322    result.placements = placements;
323    result.unplaced = unplaced;
324    result.boundaries_used = 1;
325    result.utilization = utilization;
326    result.computation_time_ms = sa_result.elapsed.as_millis() as u64;
327    result.iterations = Some(sa_result.iterations);
328    result.best_fitness = Some(sa_result.best.objective());
329    result.fitness_history = Some(sa_result.history);
330    result.strategy = Some("SimulatedAnnealing".to_string());
331    result.cancelled = cancelled.load(Ordering::Relaxed);
332    result.target_reached = sa_result.target_reached;
333
334    result
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_sa_packing_basic() {
343        let geometries = vec![
344            Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
345            Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
346        ];
347
348        let boundary = Boundary3D::new(100.0, 80.0, 50.0);
349        let config = Config::default();
350        let sa_config = SaConfig::default()
351            .with_initial_temp(100.0)
352            .with_final_temp(0.1)
353            .with_cooling_rate(0.9)
354            .with_iterations_per_temp(20)
355            .with_max_iterations(500);
356
357        let result = run_sa_packing(
358            &geometries,
359            &boundary,
360            &config,
361            sa_config,
362            Arc::new(AtomicBool::new(false)),
363        );
364
365        assert!(result.utilization > 0.0);
366        assert!(!result.placements.is_empty());
367        assert_eq!(result.strategy, Some("SimulatedAnnealing".to_string()));
368    }
369
370    #[test]
371    fn test_sa_packing_all_placed() {
372        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(4)];
373
374        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
375        let config = Config::default();
376        let sa_config = SaConfig::default()
377            .with_initial_temp(100.0)
378            .with_final_temp(0.1)
379            .with_max_iterations(1000);
380
381        let result = run_sa_packing(
382            &geometries,
383            &boundary,
384            &config,
385            sa_config,
386            Arc::new(AtomicBool::new(false)),
387        );
388
389        // All 4 boxes should fit easily
390        assert_eq!(result.placements.len(), 4);
391        assert!(result.unplaced.is_empty());
392    }
393
394    #[test]
395    fn test_sa_packing_with_orientations() {
396        use crate::geometry::OrientationConstraint;
397
398        // Long boxes that benefit from rotation
399        let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
400            .with_quantity(3)
401            .with_orientation(OrientationConstraint::Any)];
402
403        let boundary = Boundary3D::new(60.0, 60.0, 60.0);
404        let config = Config::default();
405        let sa_config = SaConfig::default()
406            .with_initial_temp(100.0)
407            .with_final_temp(0.1)
408            .with_max_iterations(500);
409
410        let result = run_sa_packing(
411            &geometries,
412            &boundary,
413            &config,
414            sa_config,
415            Arc::new(AtomicBool::new(false)),
416        );
417
418        assert!(result.utilization > 0.0);
419        assert!(!result.placements.is_empty());
420    }
421
422    #[test]
423    fn test_sa_problem_decode() {
424        use rand::prelude::*;
425
426        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2)];
427
428        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
429        let config = Config::default();
430        let cancelled = Arc::new(AtomicBool::new(false));
431
432        let problem = SaPackingProblem::new(geometries, boundary, config, cancelled);
433
434        assert_eq!(problem.num_instances(), 2);
435
436        // Create a random solution and decode
437        let mut rng = thread_rng();
438        let solution = PermutationSolution::random(problem.num_instances(), 1, &mut rng);
439        let (placements, utilization, placed_count) = problem.decode(&solution);
440
441        // Should place at least one item
442        assert!(placed_count >= 1);
443        assert_eq!(placements.len(), placed_count);
444        if placed_count > 0 {
445            assert!(utilization > 0.0);
446        }
447    }
448
449    #[test]
450    fn test_sa_packing_mass_constraint() {
451        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0)
452            .with_quantity(10)
453            .with_mass(100.0)];
454
455        let boundary = Boundary3D::new(100.0, 100.0, 100.0).with_max_mass(350.0);
456        let config = Config::default();
457        let sa_config = SaConfig::default()
458            .with_initial_temp(100.0)
459            .with_final_temp(0.1)
460            .with_max_iterations(500);
461
462        let result = run_sa_packing(
463            &geometries,
464            &boundary,
465            &config,
466            sa_config,
467            Arc::new(AtomicBool::new(false)),
468        );
469
470        // Should only place 3 boxes due to 350 mass limit
471        assert!(result.placements.len() <= 3);
472    }
473}