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