Skip to main content

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