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