Skip to main content

u_nesting_d2/
sa_nesting.rs

1//! Simulated Annealing-based 2D nesting optimization.
2//!
3//! This module provides Simulated Annealing based optimization for 2D nesting
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 rotation of an item
13
14use crate::boundary::Boundary2D;
15use crate::clamp_placement_to_boundary;
16use crate::geometry::Geometry2D;
17use crate::nfp::{
18    compute_ifp, compute_nfp, find_bottom_left_placement, verify_no_overlap, Nfp, PlacedGeometry,
19};
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::sync::Arc;
22use u_nesting_core::geometry::{Boundary, Geometry};
23use u_nesting_core::sa::{
24    NeighborhoodOperator, PermutationSolution, SaConfig, SaProblem, SaRunner, SaSolution,
25};
26use u_nesting_core::solver::Config;
27use u_nesting_core::{Placement, SolveResult};
28
29use crate::placement_utils::{expand_nfp, nesting_fitness, shrink_ifp, InstanceInfo};
30
31/// SA problem definition for 2D nesting.
32pub struct SaNestingProblem {
33    /// Input geometries.
34    geometries: Vec<Geometry2D>,
35    /// Boundary container.
36    boundary: Boundary2D,
37    /// Solver configuration.
38    config: Config,
39    /// Instance mapping (instance_id -> (geometry_idx, instance_num)).
40    instances: Vec<InstanceInfo>,
41    /// Available rotation angles per geometry.
42    rotation_angles: Vec<Vec<f64>>,
43    /// Maximum rotation options across all geometries.
44    max_rotation_options: usize,
45    /// Cancellation flag.
46    cancelled: Arc<AtomicBool>,
47}
48
49impl SaNestingProblem {
50    /// Creates a new SA nesting problem.
51    pub fn new(
52        geometries: Vec<Geometry2D>,
53        boundary: Boundary2D,
54        config: Config,
55        cancelled: Arc<AtomicBool>,
56    ) -> Self {
57        // Build instance mapping
58        let mut instances = Vec::new();
59        let mut rotation_angles = Vec::new();
60        let mut max_rotation_options = 1;
61
62        for (geom_idx, geom) in geometries.iter().enumerate() {
63            // Get rotation angles for this geometry
64            let angles = geom.rotations();
65            let angles = if angles.is_empty() { vec![0.0] } else { angles };
66            max_rotation_options = max_rotation_options.max(angles.len());
67            rotation_angles.push(angles);
68
69            // Create instances
70            for instance_num in 0..geom.quantity() {
71                instances.push(InstanceInfo {
72                    geometry_idx: geom_idx,
73                    instance_num,
74                });
75            }
76        }
77
78        Self {
79            geometries,
80            boundary,
81            config,
82            instances,
83            rotation_angles,
84            max_rotation_options,
85            cancelled,
86        }
87    }
88
89    /// Returns the total number of instances.
90    pub fn num_instances(&self) -> usize {
91        self.instances.len()
92    }
93
94    /// Decodes a solution into placements using NFP-guided placement.
95    pub fn decode(&self, solution: &PermutationSolution) -> (Vec<Placement<f64>>, f64, usize) {
96        let n = self.instances.len();
97        if n == 0 || solution.sequence.is_empty() {
98            return (Vec::new(), 0.0, 0);
99        }
100
101        let mut placements = Vec::new();
102        let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
103        let mut total_placed_area = 0.0;
104        let mut placed_count = 0;
105
106        let margin = self.config.margin;
107        let spacing = self.config.spacing;
108
109        // Get boundary polygon with margin
110        let boundary_polygon = self.get_boundary_polygon_with_margin(margin);
111
112        // Sampling step for grid search
113        let sample_step = self.compute_sample_step();
114
115        // Place geometries in the solution order
116        for (seq_idx, &instance_idx) in solution.sequence.iter().enumerate() {
117            if self.cancelled.load(Ordering::Relaxed) {
118                break;
119            }
120
121            if instance_idx >= self.instances.len() {
122                continue;
123            }
124
125            let info = &self.instances[instance_idx];
126            let geom = &self.geometries[info.geometry_idx];
127
128            // Get rotation from solution
129            let rotation_idx = solution.rotations.get(seq_idx).copied().unwrap_or(0);
130            let num_rotations = self
131                .rotation_angles
132                .get(info.geometry_idx)
133                .map(|a| a.len())
134                .unwrap_or(1);
135
136            let rotation_angle = self
137                .rotation_angles
138                .get(info.geometry_idx)
139                .and_then(|angles| angles.get(rotation_idx % num_rotations))
140                .copied()
141                .unwrap_or(0.0);
142
143            // Compute IFP for this geometry at this rotation
144            let ifp = match compute_ifp(&boundary_polygon, geom, rotation_angle) {
145                Ok(ifp) => ifp,
146                Err(_) => continue,
147            };
148
149            if ifp.is_empty() {
150                continue;
151            }
152
153            // Compute NFPs with all placed geometries
154            let mut nfps: Vec<Nfp> = Vec::new();
155            for placed in &placed_geometries {
156                let placed_exterior = placed.translated_exterior();
157                let placed_geom = Geometry2D::new(format!("_placed_{}", placed.geometry.id()))
158                    .with_polygon(placed_exterior);
159
160                if let Ok(nfp) = compute_nfp(&placed_geom, geom, rotation_angle) {
161                    let expanded = expand_nfp(&nfp, spacing);
162                    nfps.push(expanded);
163                }
164            }
165
166            // Shrink IFP by spacing
167            let ifp_shrunk = shrink_ifp(&ifp, spacing);
168
169            // Find the bottom-left valid placement
170            // IFP returns positions where the geometry's origin should be placed.
171            // Clamp to ensure placement keeps geometry within boundary.
172            let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
173            if let Some((x, y)) = find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step) {
174                // Clamp position to keep geometry within boundary
175                let geom_aabb = geom.aabb_at_rotation(rotation_angle);
176                let boundary_aabb = self.boundary.aabb();
177
178                if let Some((clamped_x, clamped_y)) =
179                    clamp_placement_to_boundary(x, y, geom_aabb, boundary_aabb)
180                {
181                    // Only verify overlap if clamping changed the position
182                    // The original NFP-found position is already collision-free by definition
183                    let was_clamped = (clamped_x - x).abs() > 1e-6 || (clamped_y - y).abs() > 1e-6;
184                    if was_clamped {
185                        // Verify no actual polygon overlap using SAT
186                        if !verify_no_overlap(
187                            geom,
188                            (clamped_x, clamped_y),
189                            rotation_angle,
190                            &placed_geometries,
191                        ) {
192                            continue; // Skip - clamped position would cause overlap
193                        }
194                    }
195
196                    let placement = Placement::new_2d(
197                        geom.id().clone(),
198                        info.instance_num,
199                        clamped_x,
200                        clamped_y,
201                        rotation_angle,
202                    );
203
204                    placements.push(placement);
205                    placed_geometries.push(PlacedGeometry::new(
206                        geom.clone(),
207                        (clamped_x, clamped_y),
208                        rotation_angle,
209                    ));
210                    total_placed_area += geom.measure();
211                    placed_count += 1;
212                }
213            }
214        }
215
216        let utilization = total_placed_area / self.boundary.measure();
217        (placements, utilization, placed_count)
218    }
219
220    /// Gets the boundary polygon with margin applied.
221    fn get_boundary_polygon_with_margin(&self, margin: f64) -> Vec<(f64, f64)> {
222        let (b_min, b_max) = self.boundary.aabb();
223        vec![
224            (b_min[0] + margin, b_min[1] + margin),
225            (b_max[0] - margin, b_min[1] + margin),
226            (b_max[0] - margin, b_max[1] - margin),
227            (b_min[0] + margin, b_max[1] - margin),
228        ]
229    }
230
231    /// Computes an adaptive sample step based on geometry sizes.
232    fn compute_sample_step(&self) -> f64 {
233        if self.geometries.is_empty() {
234            return 1.0;
235        }
236
237        let mut min_dim = f64::INFINITY;
238        for geom in &self.geometries {
239            let (g_min, g_max) = geom.aabb();
240            let width = g_max[0] - g_min[0];
241            let height = g_max[1] - g_min[1];
242            min_dim = min_dim.min(width).min(height);
243        }
244
245        (min_dim / 4.0).clamp(0.5, 10.0)
246    }
247}
248
249impl SaProblem for SaNestingProblem {
250    type Solution = PermutationSolution;
251
252    fn initial_solution<R: rand::Rng>(&self, rng: &mut R) -> Self::Solution {
253        PermutationSolution::random(self.instances.len(), self.max_rotation_options, rng)
254    }
255
256    fn neighbor<R: rand::Rng>(
257        &self,
258        solution: &Self::Solution,
259        operator: NeighborhoodOperator,
260        rng: &mut R,
261    ) -> Self::Solution {
262        match operator {
263            NeighborhoodOperator::Swap => solution.apply_swap(rng),
264            NeighborhoodOperator::Relocate => solution.apply_relocate(rng),
265            NeighborhoodOperator::Inversion => solution.apply_inversion(rng),
266            NeighborhoodOperator::Rotation => solution.apply_rotation(rng),
267            NeighborhoodOperator::Chain => solution.apply_chain(rng),
268        }
269    }
270
271    fn evaluate(&self, solution: &mut Self::Solution) {
272        let (_, utilization, placed_count) = self.decode(solution);
273        let fitness = nesting_fitness(placed_count, self.instances.len(), utilization);
274        solution.set_objective(fitness);
275    }
276
277    fn available_operators(&self) -> Vec<NeighborhoodOperator> {
278        if self.max_rotation_options > 1 {
279            vec![
280                NeighborhoodOperator::Swap,
281                NeighborhoodOperator::Relocate,
282                NeighborhoodOperator::Inversion,
283                NeighborhoodOperator::Rotation,
284                NeighborhoodOperator::Chain,
285            ]
286        } else {
287            vec![
288                NeighborhoodOperator::Swap,
289                NeighborhoodOperator::Relocate,
290                NeighborhoodOperator::Inversion,
291                NeighborhoodOperator::Chain,
292            ]
293        }
294    }
295
296    fn on_temperature_change(
297        &self,
298        temperature: f64,
299        iteration: u64,
300        best: &Self::Solution,
301        _current: &Self::Solution,
302    ) {
303        log::debug!(
304            "SA Iteration {}: temp={:.4}, best_fitness={:.4}",
305            iteration,
306            temperature,
307            best.objective()
308        );
309    }
310}
311
312/// Runs SA-based nesting optimization.
313pub fn run_sa_nesting(
314    geometries: &[Geometry2D],
315    boundary: &Boundary2D,
316    config: &Config,
317    sa_config: SaConfig,
318    cancelled: Arc<AtomicBool>,
319) -> SolveResult<f64> {
320    let problem = SaNestingProblem::new(
321        geometries.to_vec(),
322        boundary.clone(),
323        config.clone(),
324        cancelled.clone(),
325    );
326
327    let runner = SaRunner::new(sa_config, problem);
328
329    // Set cancellation
330    let cancel_handle = runner.cancel_handle();
331    let cancelled_clone = cancelled.clone();
332    std::thread::spawn(move || {
333        while !cancelled_clone.load(Ordering::Relaxed) {
334            std::thread::sleep(std::time::Duration::from_millis(100));
335        }
336        cancel_handle.store(true, Ordering::Relaxed);
337    });
338
339    let sa_result = runner.run();
340
341    // Decode the best solution to get final placements
342    let problem = SaNestingProblem::new(
343        geometries.to_vec(),
344        boundary.clone(),
345        config.clone(),
346        Arc::new(AtomicBool::new(false)),
347    );
348
349    let (placements, utilization, _placed_count) = problem.decode(&sa_result.best);
350
351    // Build unplaced list
352    let mut unplaced = Vec::new();
353    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
354    for p in &placements {
355        placed_ids.insert(p.geometry_id.clone());
356    }
357    for geom in geometries {
358        if !placed_ids.contains(geom.id()) {
359            unplaced.push(geom.id().clone());
360        }
361    }
362
363    let mut result = SolveResult::new();
364    result.placements = placements;
365    result.unplaced = unplaced;
366    result.boundaries_used = 1;
367    result.utilization = utilization;
368    result.computation_time_ms = sa_result.elapsed.as_millis() as u64;
369    result.iterations = Some(sa_result.iterations);
370    result.best_fitness = Some(sa_result.best.objective());
371    result.fitness_history = Some(sa_result.history);
372    result.strategy = Some("SimulatedAnnealing".to_string());
373    result.cancelled = cancelled.load(Ordering::Relaxed);
374    result.target_reached = sa_result.target_reached;
375
376    result
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    #[test]
384    fn test_sa_nesting_basic() {
385        let geometries = vec![
386            Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
387            Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
388        ];
389
390        let boundary = Boundary2D::rectangle(100.0, 50.0);
391        let config = Config::default();
392        let sa_config = SaConfig::default()
393            .with_initial_temp(100.0)
394            .with_final_temp(0.1)
395            .with_cooling_rate(0.9)
396            .with_iterations_per_temp(20)
397            .with_max_iterations(500);
398
399        let result = run_sa_nesting(
400            &geometries,
401            &boundary,
402            &config,
403            sa_config,
404            Arc::new(AtomicBool::new(false)),
405        );
406
407        assert!(result.utilization > 0.0);
408        assert!(!result.placements.is_empty());
409        assert_eq!(result.strategy, Some("SimulatedAnnealing".to_string()));
410    }
411
412    #[test]
413    fn test_sa_nesting_all_placed() {
414        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
415
416        let boundary = Boundary2D::rectangle(100.0, 100.0);
417        let config = Config::default();
418        let sa_config = SaConfig::default()
419            .with_initial_temp(100.0)
420            .with_final_temp(0.1)
421            .with_max_iterations(1000);
422
423        let result = run_sa_nesting(
424            &geometries,
425            &boundary,
426            &config,
427            sa_config,
428            Arc::new(AtomicBool::new(false)),
429        );
430
431        // All 4 pieces should fit easily
432        assert_eq!(result.placements.len(), 4);
433        assert!(result.unplaced.is_empty());
434    }
435
436    #[test]
437    fn test_sa_nesting_with_rotation() {
438        let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
439            .with_quantity(3)
440            .with_rotations(vec![0.0, 90.0])];
441
442        let boundary = Boundary2D::rectangle(50.0, 50.0);
443        let config = Config::default();
444        let sa_config = SaConfig::default()
445            .with_initial_temp(100.0)
446            .with_final_temp(0.1)
447            .with_max_iterations(500);
448
449        let result = run_sa_nesting(
450            &geometries,
451            &boundary,
452            &config,
453            sa_config,
454            Arc::new(AtomicBool::new(false)),
455        );
456
457        assert!(result.utilization > 0.0);
458        assert!(!result.placements.is_empty());
459    }
460
461    #[test]
462    fn test_sa_problem_decode() {
463        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2)];
464
465        let boundary = Boundary2D::rectangle(100.0, 50.0);
466        let config = Config::default();
467        let cancelled = Arc::new(AtomicBool::new(false));
468
469        let problem = SaNestingProblem::new(geometries, boundary, config, cancelled);
470
471        assert_eq!(problem.num_instances(), 2);
472
473        // Create a random solution and decode
474        let mut rng = rand::rng();
475        let solution = PermutationSolution::random(problem.num_instances(), 1, &mut rng);
476        let (placements, utilization, placed_count) = problem.decode(&solution);
477
478        // Should place at least one item
479        assert!(placed_count >= 1);
480        assert_eq!(placements.len(), placed_count);
481        if placed_count > 0 {
482            assert!(utilization > 0.0);
483        }
484    }
485}