u_nesting_d2/
alns_nesting.rs

1//! Adaptive Large Neighborhood Search (ALNS) based 2D nesting optimization.
2//!
3//! This module provides ALNS-based optimization for 2D nesting problems,
4//! implementing the algorithm from Ropke & Pisinger (2006).
5//!
6//! # Destroy Operators
7//!
8//! - **Random**: Remove random items from the solution
9//! - **Worst**: Remove items with worst placement scores
10//! - **Related**: Remove items similar to a seed item
11//! - **Shaw**: Remove items based on spatial clustering
12//!
13//! # Repair Operators
14//!
15//! - **Greedy**: Place items at best available position
16//! - **Regret**: Use regret-based insertion
17//! - **Random**: Place items in random valid positions
18//! - **BLF**: Use bottom-left fill heuristic
19
20use crate::boundary::Boundary2D;
21use crate::clamp_placement_to_boundary;
22use crate::geometry::Geometry2D;
23use crate::nfp::{compute_ifp, compute_nfp, find_bottom_left_placement, Nfp, PlacedGeometry};
24use std::sync::atomic::{AtomicBool, Ordering};
25use std::sync::Arc;
26use std::time::Instant;
27use u_nesting_core::alns::{
28    AlnsConfig, AlnsProblem, AlnsResult, AlnsRunner, AlnsSolution, DestroyOperatorId,
29    DestroyResult, RepairOperatorId, RepairResult,
30};
31use u_nesting_core::geometry::{Boundary, Geometry};
32use u_nesting_core::solver::Config;
33use u_nesting_core::{Placement, SolveResult};
34
35use rand::prelude::*;
36
37/// Instance information for decoding.
38#[derive(Debug, Clone)]
39struct InstanceInfo {
40    /// Index into the geometries array.
41    geometry_idx: usize,
42    /// Instance number within this geometry's quantity.
43    #[allow(dead_code)]
44    instance_num: usize,
45}
46
47/// A placed item in the ALNS solution.
48#[derive(Debug, Clone)]
49pub struct PlacedItem {
50    /// Instance index.
51    pub instance_idx: usize,
52    /// X position.
53    pub x: f64,
54    /// Y position.
55    pub y: f64,
56    /// Rotation angle in radians.
57    pub rotation: f64,
58    /// Placement score (lower = better).
59    pub score: f64,
60}
61
62/// ALNS solution for 2D nesting.
63#[derive(Debug, Clone)]
64pub struct AlnsNestingSolution {
65    /// Placed items.
66    pub placed: Vec<PlacedItem>,
67    /// Unplaced instance indices.
68    pub unplaced: Vec<usize>,
69    /// Total number of instances.
70    pub total_instances: usize,
71    /// Total placed area.
72    pub placed_area: f64,
73    /// Boundary area.
74    pub boundary_area: f64,
75    /// Maximum Y coordinate used (strip height).
76    pub max_y: f64,
77}
78
79impl AlnsNestingSolution {
80    /// Create a new empty solution.
81    pub fn new(total_instances: usize, boundary_area: f64) -> Self {
82        Self {
83            placed: Vec::new(),
84            unplaced: (0..total_instances).collect(),
85            total_instances,
86            placed_area: 0.0,
87            boundary_area,
88            max_y: 0.0,
89        }
90    }
91}
92
93impl AlnsSolution for AlnsNestingSolution {
94    fn fitness(&self) -> f64 {
95        // Fitness combines unplaced penalty + utilization + height
96        let unplaced_penalty = self.unplaced.len() as f64 * 1000.0;
97        let utilization_penalty = if self.placed_area > 0.0 {
98            1.0 - (self.placed_area / self.boundary_area)
99        } else {
100            1.0
101        };
102        let height_penalty = self.max_y / 1000.0;
103
104        unplaced_penalty + utilization_penalty + height_penalty
105    }
106
107    fn placed_count(&self) -> usize {
108        self.placed.len()
109    }
110
111    fn total_count(&self) -> usize {
112        self.total_instances
113    }
114}
115
116/// ALNS problem definition for 2D nesting.
117pub struct AlnsNestingProblem {
118    /// Input geometries.
119    geometries: Vec<Geometry2D>,
120    /// Boundary container.
121    boundary: Boundary2D,
122    /// Solver configuration.
123    config: Config,
124    /// Instance mapping.
125    instances: Vec<InstanceInfo>,
126    /// Available rotation angles per geometry.
127    rotation_angles: Vec<Vec<f64>>,
128    /// Geometry areas.
129    geometry_areas: Vec<f64>,
130    /// Cancellation flag.
131    cancelled: Arc<AtomicBool>,
132    /// Start time for timeout checking.
133    start_time: Instant,
134    /// Time limit in milliseconds.
135    time_limit_ms: u64,
136}
137
138impl AlnsNestingProblem {
139    /// Creates a new ALNS nesting problem.
140    pub fn new(
141        geometries: Vec<Geometry2D>,
142        boundary: Boundary2D,
143        config: Config,
144        cancelled: Arc<AtomicBool>,
145        time_limit_ms: u64,
146    ) -> Self {
147        let mut instances = Vec::new();
148        let mut rotation_angles = Vec::new();
149        let mut geometry_areas = Vec::new();
150
151        for (geom_idx, geom) in geometries.iter().enumerate() {
152            let angles = geom.rotations();
153            let angles = if angles.is_empty() { vec![0.0] } else { angles };
154            rotation_angles.push(angles);
155
156            let area = geom.measure();
157            geometry_areas.push(area);
158
159            for instance_num in 0..geom.quantity() {
160                instances.push(InstanceInfo {
161                    geometry_idx: geom_idx,
162                    instance_num,
163                });
164            }
165        }
166
167        Self {
168            geometries,
169            boundary,
170            config,
171            instances,
172            rotation_angles,
173            geometry_areas,
174            cancelled,
175            start_time: Instant::now(),
176            time_limit_ms,
177        }
178    }
179
180    /// Check if timeout has been reached.
181    fn is_timed_out(&self) -> bool {
182        if self.time_limit_ms == 0 {
183            return false;
184        }
185        self.start_time.elapsed().as_millis() as u64 >= self.time_limit_ms
186    }
187
188    /// Returns the total number of instances.
189    pub fn num_instances(&self) -> usize {
190        self.instances.len()
191    }
192
193    /// Get boundary polygon with margin.
194    fn get_boundary_polygon_with_margin(&self, margin: f64) -> Vec<(f64, f64)> {
195        let (min, max) = self.boundary.aabb();
196        vec![
197            (min[0] + margin, min[1] + margin),
198            (max[0] - margin, min[1] + margin),
199            (max[0] - margin, max[1] - margin),
200            (min[0] + margin, max[1] - margin),
201        ]
202    }
203
204    /// Compute sample step for grid search.
205    fn compute_sample_step(&self) -> f64 {
206        let (min, max) = self.boundary.aabb();
207        let width = max[0] - min[0];
208        (width / 100.0).max(1.0)
209    }
210
211    /// Try to place an item at the best position using NFP.
212    fn try_place_item(
213        &self,
214        instance_idx: usize,
215        placed_geometries: &[PlacedGeometry],
216        boundary_polygon: &[(f64, f64)],
217        sample_step: f64,
218    ) -> Option<PlacedItem> {
219        let info = &self.instances[instance_idx];
220        let geom = &self.geometries[info.geometry_idx];
221        let angles = &self.rotation_angles[info.geometry_idx];
222
223        let mut best_placement: Option<PlacedItem> = None;
224        let mut best_y = f64::MAX;
225
226        for &rotation in angles {
227            let ifp = match compute_ifp(boundary_polygon, geom, rotation) {
228                Ok(ifp) => ifp,
229                Err(_) => continue,
230            };
231
232            if ifp.is_empty() {
233                continue;
234            }
235
236            let spacing = self.config.spacing;
237            let mut nfps: Vec<Nfp> = Vec::new();
238
239            for pg in placed_geometries {
240                let placed_exterior = pg.translated_exterior();
241                let placed_geom = Geometry2D::new(format!("_placed_{}", pg.geometry.id()))
242                    .with_polygon(placed_exterior);
243
244                if let Ok(nfp) = compute_nfp(&placed_geom, geom, rotation) {
245                    let expanded = expand_nfp(&nfp, spacing);
246                    nfps.push(expanded);
247                }
248            }
249
250            let ifp_shrunk = shrink_ifp(&ifp, spacing);
251            let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
252
253            // IFP returns positions where the geometry's origin should be placed.
254            // Clamp to ensure placement keeps geometry within boundary.
255            if let Some((x, y)) = find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step) {
256                // Clamp position to keep geometry within boundary
257                let geom_aabb = geom.aabb_at_rotation(rotation);
258                let boundary_aabb = self.boundary.aabb();
259
260                if let Some((clamped_x, clamped_y)) =
261                    clamp_placement_to_boundary(x, y, geom_aabb, boundary_aabb)
262                {
263                    if clamped_y < best_y {
264                        best_y = clamped_y;
265                        best_placement = Some(PlacedItem {
266                            instance_idx,
267                            x: clamped_x,
268                            y: clamped_y,
269                            rotation,
270                            score: clamped_y,
271                        });
272                    }
273                }
274            }
275        }
276
277        best_placement
278    }
279
280    /// Place items using BLF heuristic.
281    fn place_items_blf(&self, items: &[usize], solution: &mut AlnsNestingSolution) {
282        let margin = self.config.margin;
283        let boundary_polygon = self.get_boundary_polygon_with_margin(margin);
284        let sample_step = self.compute_sample_step();
285
286        let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
287        for item in &solution.placed {
288            let info = &self.instances[item.instance_idx];
289            let geom = &self.geometries[info.geometry_idx];
290            placed_geometries.push(PlacedGeometry {
291                geometry: geom.clone(),
292                position: (item.x, item.y),
293                rotation: item.rotation,
294            });
295        }
296
297        // Sort items by area (largest first)
298        let mut sorted_items = items.to_vec();
299        sorted_items.sort_by(|&a, &b| {
300            let area_a = self.geometry_areas[self.instances[a].geometry_idx];
301            let area_b = self.geometry_areas[self.instances[b].geometry_idx];
302            area_b
303                .partial_cmp(&area_a)
304                .unwrap_or(std::cmp::Ordering::Equal)
305        });
306
307        for &instance_idx in &sorted_items {
308            // Check cancellation and timeout
309            if self.cancelled.load(Ordering::Relaxed) || self.is_timed_out() {
310                break;
311            }
312
313            if let Some(placement) = self.try_place_item(
314                instance_idx,
315                &placed_geometries,
316                &boundary_polygon,
317                sample_step,
318            ) {
319                let info = &self.instances[instance_idx];
320                let area = self.geometry_areas[info.geometry_idx];
321
322                solution.placed_area += area;
323                solution.max_y = solution.max_y.max(placement.y);
324
325                let geom = &self.geometries[info.geometry_idx];
326                placed_geometries.push(PlacedGeometry {
327                    geometry: geom.clone(),
328                    position: (placement.x, placement.y),
329                    rotation: placement.rotation,
330                });
331
332                solution.placed.push(placement);
333                solution.unplaced.retain(|&idx| idx != instance_idx);
334            }
335        }
336    }
337
338    /// Remove item from solution.
339    fn remove_item(&self, solution: &mut AlnsNestingSolution, instance_idx: usize) {
340        if let Some(pos) = solution
341            .placed
342            .iter()
343            .position(|p| p.instance_idx == instance_idx)
344        {
345            let item = solution.placed.remove(pos);
346            let info = &self.instances[item.instance_idx];
347            solution.placed_area -= self.geometry_areas[info.geometry_idx];
348            solution.unplaced.push(item.instance_idx);
349        }
350
351        // Recalculate max_y
352        solution.max_y = solution.placed.iter().map(|p| p.y).fold(0.0, f64::max);
353    }
354}
355
356impl AlnsProblem for AlnsNestingProblem {
357    type Solution = AlnsNestingSolution;
358
359    fn create_initial_solution(&mut self) -> AlnsNestingSolution {
360        let boundary_area = self.boundary.measure();
361        let mut solution = AlnsNestingSolution::new(self.instances.len(), boundary_area);
362
363        let all_items: Vec<usize> = (0..self.instances.len()).collect();
364        self.place_items_blf(&all_items, &mut solution);
365
366        solution
367    }
368
369    fn clone_solution(&self, solution: &AlnsNestingSolution) -> AlnsNestingSolution {
370        solution.clone()
371    }
372
373    fn destroy_operators(&self) -> Vec<DestroyOperatorId> {
374        vec![
375            DestroyOperatorId::Random,
376            DestroyOperatorId::Worst,
377            DestroyOperatorId::Related,
378            DestroyOperatorId::Shaw,
379        ]
380    }
381
382    fn repair_operators(&self) -> Vec<RepairOperatorId> {
383        vec![
384            RepairOperatorId::Greedy,
385            RepairOperatorId::BottomLeftFill,
386            RepairOperatorId::Random,
387        ]
388    }
389
390    fn destroy(
391        &mut self,
392        solution: &mut AlnsNestingSolution,
393        operator: DestroyOperatorId,
394        degree: f64,
395        rng: &mut rand::rngs::StdRng,
396    ) -> DestroyResult {
397        let num_to_remove = ((solution.placed.len() as f64 * degree).ceil() as usize).max(1);
398        let mut removed_indices = Vec::new();
399
400        if solution.placed.is_empty() {
401            return DestroyResult {
402                removed_indices,
403                operator,
404            };
405        }
406
407        match operator {
408            DestroyOperatorId::Random => {
409                // Random removal
410                let mut indices: Vec<usize> =
411                    solution.placed.iter().map(|p| p.instance_idx).collect();
412                indices.shuffle(rng);
413
414                for &idx in indices.iter().take(num_to_remove) {
415                    removed_indices.push(idx);
416                }
417            }
418            DestroyOperatorId::Worst => {
419                // Worst removal (highest Y position = worst)
420                let mut items_with_score: Vec<(usize, f64)> = solution
421                    .placed
422                    .iter()
423                    .map(|p| (p.instance_idx, p.score))
424                    .collect();
425
426                items_with_score
427                    .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
428
429                for (idx, _) in items_with_score.iter().take(num_to_remove) {
430                    removed_indices.push(*idx);
431                }
432            }
433            DestroyOperatorId::Related | DestroyOperatorId::Shaw => {
434                // Cluster removal (same logic for both)
435                let seed_idx = rng.gen_range(0..solution.placed.len());
436                let seed = &solution.placed[seed_idx];
437                let seed_x = seed.x;
438                let seed_y = seed.y;
439
440                let mut items_with_distance: Vec<(usize, f64)> = solution
441                    .placed
442                    .iter()
443                    .map(|item| {
444                        let dx = item.x - seed_x;
445                        let dy = item.y - seed_y;
446                        (item.instance_idx, (dx * dx + dy * dy).sqrt())
447                    })
448                    .collect();
449
450                items_with_distance
451                    .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
452
453                for (idx, _) in items_with_distance.iter().take(num_to_remove) {
454                    removed_indices.push(*idx);
455                }
456            }
457            DestroyOperatorId::Custom(_) => {
458                // Fall back to random for custom operators
459                let mut indices: Vec<usize> =
460                    solution.placed.iter().map(|p| p.instance_idx).collect();
461                indices.shuffle(rng);
462
463                for &idx in indices.iter().take(num_to_remove) {
464                    removed_indices.push(idx);
465                }
466            }
467        }
468
469        // Remove items from solution
470        for &idx in &removed_indices {
471            self.remove_item(solution, idx);
472        }
473
474        DestroyResult {
475            removed_indices,
476            operator,
477        }
478    }
479
480    fn repair(
481        &mut self,
482        solution: &mut AlnsNestingSolution,
483        _destroyed: &DestroyResult,
484        operator: RepairOperatorId,
485    ) -> RepairResult {
486        let items_to_place = solution.unplaced.clone();
487        let initial_placed = solution.placed.len();
488
489        match operator {
490            RepairOperatorId::Greedy | RepairOperatorId::BottomLeftFill => {
491                // BLF is already greedy for bottom-left positions
492                self.place_items_blf(&items_to_place, solution);
493            }
494            RepairOperatorId::Regret => {
495                // Regret-based insertion (simplified: use BLF for now)
496                self.place_items_blf(&items_to_place, solution);
497            }
498            RepairOperatorId::Random => {
499                // Random order placement
500                let mut shuffled = items_to_place.clone();
501                use rand::SeedableRng;
502                let mut rng = rand::rngs::StdRng::from_entropy();
503                shuffled.shuffle(&mut rng);
504                self.place_items_blf(&shuffled, solution);
505            }
506            RepairOperatorId::Custom(_) => {
507                self.place_items_blf(&items_to_place, solution);
508            }
509        }
510
511        RepairResult {
512            placed_count: solution.placed.len() - initial_placed,
513            unplaced_count: solution.unplaced.len(),
514            operator,
515        }
516    }
517
518    fn relatedness(&self, solution: &AlnsNestingSolution, i: usize, j: usize) -> f64 {
519        // Relatedness based on spatial distance
520        let item_i = solution.placed.iter().find(|p| p.instance_idx == i);
521        let item_j = solution.placed.iter().find(|p| p.instance_idx == j);
522
523        match (item_i, item_j) {
524            (Some(a), Some(b)) => {
525                let dx = a.x - b.x;
526                let dy = a.y - b.y;
527                1.0 / (1.0 + (dx * dx + dy * dy).sqrt())
528            }
529            _ => 0.0,
530        }
531    }
532}
533
534/// Run ALNS nesting optimization.
535pub fn run_alns_nesting(
536    geometries: &[Geometry2D],
537    boundary: &Boundary2D,
538    config: &Config,
539    alns_config: &AlnsConfig,
540    cancelled: Arc<AtomicBool>,
541) -> SolveResult<f64> {
542    let mut problem = AlnsNestingProblem::new(
543        geometries.to_vec(),
544        boundary.clone(),
545        config.clone(),
546        cancelled,
547        alns_config.time_limit_ms,
548    );
549
550    let runner = AlnsRunner::new(alns_config.clone());
551    let alns_result: AlnsResult<AlnsNestingSolution> = runner.run(&mut problem, |_progress| {
552        // Progress callback
553    });
554
555    let mut result = SolveResult::new();
556
557    for item in &alns_result.best_solution.placed {
558        let info = &problem.instances[item.instance_idx];
559        let geom = &problem.geometries[info.geometry_idx];
560
561        result.placements.push(Placement::new_2d(
562            geom.id().to_string(),
563            info.instance_num,
564            item.x,
565            item.y,
566            item.rotation,
567        ));
568    }
569
570    result.boundaries_used = if result.placements.is_empty() { 0 } else { 1 };
571    result.utilization =
572        alns_result.best_solution.placed_area / alns_result.best_solution.boundary_area;
573    result.computation_time_ms = alns_result.elapsed_ms;
574    result.iterations = Some(alns_result.iterations as u64);
575    result.best_fitness = Some(alns_result.best_fitness);
576    result.strategy = Some("ALNS".to_string());
577
578    result
579}
580
581/// Expands an NFP by the given spacing amount.
582fn expand_nfp(nfp: &Nfp, spacing: f64) -> Nfp {
583    if spacing <= 0.0 {
584        return nfp.clone();
585    }
586
587    let expanded_polygons: Vec<Vec<(f64, f64)>> = nfp
588        .polygons
589        .iter()
590        .map(|polygon| {
591            let (cx, cy) = polygon_centroid(polygon);
592            polygon
593                .iter()
594                .map(|&(x, y)| {
595                    let dx = x - cx;
596                    let dy = y - cy;
597                    let dist = (dx * dx + dy * dy).sqrt();
598                    if dist > 1e-10 {
599                        let scale = (dist + spacing) / dist;
600                        (cx + dx * scale, cy + dy * scale)
601                    } else {
602                        (x, y)
603                    }
604                })
605                .collect()
606        })
607        .collect();
608
609    Nfp::from_polygons(expanded_polygons)
610}
611
612/// Shrinks an IFP by the given spacing amount.
613fn shrink_ifp(ifp: &Nfp, spacing: f64) -> Nfp {
614    if spacing <= 0.0 {
615        return ifp.clone();
616    }
617
618    let shrunk_polygons: Vec<Vec<(f64, f64)>> = ifp
619        .polygons
620        .iter()
621        .filter_map(|polygon| {
622            let (cx, cy) = polygon_centroid(polygon);
623            let shrunk: Vec<(f64, f64)> = polygon
624                .iter()
625                .map(|&(x, y)| {
626                    let dx = x - cx;
627                    let dy = y - cy;
628                    let dist = (dx * dx + dy * dy).sqrt();
629                    if dist > spacing + 1e-10 {
630                        let scale = (dist - spacing) / dist;
631                        (cx + dx * scale, cy + dy * scale)
632                    } else {
633                        (cx, cy)
634                    }
635                })
636                .collect();
637
638            if shrunk.len() >= 3 {
639                Some(shrunk)
640            } else {
641                None
642            }
643        })
644        .collect();
645
646    Nfp::from_polygons(shrunk_polygons)
647}
648
649/// Computes the centroid of a polygon.
650fn polygon_centroid(polygon: &[(f64, f64)]) -> (f64, f64) {
651    if polygon.is_empty() {
652        return (0.0, 0.0);
653    }
654
655    let sum: (f64, f64) = polygon
656        .iter()
657        .fold((0.0, 0.0), |acc, &(x, y)| (acc.0 + x, acc.1 + y));
658    let n = polygon.len() as f64;
659    (sum.0 / n, sum.1 / n)
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665
666    fn create_test_geometries() -> Vec<Geometry2D> {
667        vec![
668            Geometry2D::rectangle("rect1", 50.0, 30.0).with_quantity(3),
669            Geometry2D::rectangle("rect2", 40.0, 40.0).with_quantity(2),
670            Geometry2D::rectangle("rect3", 60.0, 20.0).with_quantity(2),
671        ]
672    }
673
674    fn create_test_boundary() -> Boundary2D {
675        Boundary2D::rectangle(300.0, 200.0)
676    }
677
678    #[test]
679    fn test_alns_nesting_problem_creation() {
680        let geometries = create_test_geometries();
681        let boundary = create_test_boundary();
682        let config = Config::default();
683        let cancelled = Arc::new(AtomicBool::new(false));
684
685        let problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
686
687        assert_eq!(problem.num_instances(), 7);
688    }
689
690    #[test]
691    fn test_alns_nesting_initial_solution() {
692        let geometries = create_test_geometries();
693        let boundary = create_test_boundary();
694        let config = Config::default();
695        let cancelled = Arc::new(AtomicBool::new(false));
696
697        let mut problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
698        let solution = problem.create_initial_solution();
699
700        assert!(!solution.placed.is_empty());
701        assert!(solution.placed_area > 0.0);
702    }
703
704    #[test]
705    fn test_alns_nesting_solution_fitness() {
706        let solution = AlnsNestingSolution {
707            placed: vec![
708                PlacedItem {
709                    instance_idx: 0,
710                    x: 10.0,
711                    y: 10.0,
712                    rotation: 0.0,
713                    score: 10.0,
714                },
715                PlacedItem {
716                    instance_idx: 1,
717                    x: 60.0,
718                    y: 10.0,
719                    rotation: 0.0,
720                    score: 10.0,
721                },
722            ],
723            unplaced: vec![2],
724            total_instances: 3,
725            placed_area: 3000.0,
726            boundary_area: 60000.0,
727            max_y: 50.0,
728        };
729
730        let fitness = solution.fitness();
731        assert!(fitness > 0.0);
732        assert!(fitness >= 1000.0); // 1 unplaced item penalty
733    }
734
735    #[test]
736    fn test_alns_nesting_destroy_random() {
737        use rand::SeedableRng;
738
739        let geometries = create_test_geometries();
740        let boundary = create_test_boundary();
741        let config = Config::default();
742        let cancelled = Arc::new(AtomicBool::new(false));
743
744        let mut problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
745        let mut solution = problem.create_initial_solution();
746
747        let initial_placed = solution.placed.len();
748        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
749
750        let result = problem.destroy(&mut solution, DestroyOperatorId::Random, 0.3, &mut rng);
751
752        assert!(!result.removed_indices.is_empty());
753        assert_eq!(result.operator, DestroyOperatorId::Random);
754        assert!(solution.placed.len() < initial_placed);
755    }
756
757    #[test]
758    fn test_alns_nesting_destroy_worst() {
759        use rand::SeedableRng;
760
761        let geometries = create_test_geometries();
762        let boundary = create_test_boundary();
763        let config = Config::default();
764        let cancelled = Arc::new(AtomicBool::new(false));
765
766        let mut problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
767        let mut solution = problem.create_initial_solution();
768
769        let initial_placed = solution.placed.len();
770        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
771
772        let result = problem.destroy(&mut solution, DestroyOperatorId::Worst, 0.3, &mut rng);
773
774        assert!(!result.removed_indices.is_empty());
775        assert_eq!(result.operator, DestroyOperatorId::Worst);
776        assert!(solution.placed.len() < initial_placed);
777    }
778
779    #[test]
780    fn test_alns_nesting_repair() {
781        use rand::SeedableRng;
782
783        let geometries = create_test_geometries();
784        let boundary = create_test_boundary();
785        let config = Config::default();
786        let cancelled = Arc::new(AtomicBool::new(false));
787
788        let mut problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
789        let mut solution = problem.create_initial_solution();
790
791        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
792
793        let destroy_result =
794            problem.destroy(&mut solution, DestroyOperatorId::Random, 0.5, &mut rng);
795        let after_destroy_placed = solution.placed.len();
796
797        let repair_result =
798            problem.repair(&mut solution, &destroy_result, RepairOperatorId::Greedy);
799
800        assert!(repair_result.placed_count > 0 || after_destroy_placed == solution.placed.len());
801        assert_eq!(repair_result.operator, RepairOperatorId::Greedy);
802    }
803
804    #[test]
805    fn test_run_alns_nesting() {
806        let geometries = create_test_geometries();
807        let boundary = create_test_boundary();
808        let config = Config::default();
809        let alns_config = AlnsConfig::new()
810            .with_max_iterations(50)
811            .with_time_limit_ms(5000);
812        let cancelled = Arc::new(AtomicBool::new(false));
813
814        let result = run_alns_nesting(&geometries, &boundary, &config, &alns_config, cancelled);
815
816        assert!(!result.placements.is_empty());
817        assert!(result.utilization > 0.0);
818    }
819
820    #[test]
821    fn test_alns_nesting_full_cycle() {
822        let geometries = create_test_geometries();
823        let boundary = create_test_boundary();
824        let config = Config::default();
825        let cancelled = Arc::new(AtomicBool::new(false));
826
827        let mut problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
828
829        let alns_config = AlnsConfig::new().with_max_iterations(10).with_seed(42);
830
831        let runner = AlnsRunner::new(alns_config);
832        let result: AlnsResult<AlnsNestingSolution> = runner.run(&mut problem, |progress| {
833            assert!(progress.iteration <= 10);
834        });
835
836        assert!(result.iterations <= 10);
837        assert!(!result.best_solution.placed.is_empty());
838    }
839
840    #[test]
841    fn test_alns_destroy_operators() {
842        let geometries = create_test_geometries();
843        let boundary = create_test_boundary();
844        let config = Config::default();
845        let cancelled = Arc::new(AtomicBool::new(false));
846
847        let problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
848        let operators = problem.destroy_operators();
849
850        assert!(operators.contains(&DestroyOperatorId::Random));
851        assert!(operators.contains(&DestroyOperatorId::Worst));
852        assert!(operators.contains(&DestroyOperatorId::Related));
853        assert!(operators.contains(&DestroyOperatorId::Shaw));
854    }
855
856    #[test]
857    fn test_alns_repair_operators() {
858        let geometries = create_test_geometries();
859        let boundary = create_test_boundary();
860        let config = Config::default();
861        let cancelled = Arc::new(AtomicBool::new(false));
862
863        let problem = AlnsNestingProblem::new(geometries, boundary, config, cancelled, 60000);
864        let operators = problem.repair_operators();
865
866        assert!(operators.contains(&RepairOperatorId::Greedy));
867        assert!(operators.contains(&RepairOperatorId::BottomLeftFill));
868        assert!(operators.contains(&RepairOperatorId::Random));
869    }
870}