Skip to main content

u_nesting_d2/
gdrr_nesting.rs

1//! Goal-Driven Ruin and Recreate (GDRR) based 2D nesting optimization.
2//!
3//! This module provides GDRR-based optimization for 2D nesting problems,
4//! implementing the algorithm from Gardeyn & Wauters (EJOR 2022).
5//!
6//! # Ruin Operators
7//!
8//! - **Random**: Remove random items from the solution
9//! - **Cluster**: Remove spatially clustered items
10//! - **Worst**: Remove items with worst placement scores
11//!
12//! # Recreate Operators
13//!
14//! - **BestFit**: Place items using best-fit decreasing by area
15//! - **BLF**: Use bottom-left fill heuristic
16//! - **NFP**: NFP-guided placement for optimal positioning
17
18use crate::boundary::Boundary2D;
19use crate::clamp_placement_to_boundary;
20use crate::geometry::Geometry2D;
21use crate::nfp::{
22    compute_ifp_with_margin_and_mirror, compute_nfp_mirrored, find_bottom_left_placement,
23    verify_no_overlap_mirrored, Nfp, PlacedGeometry,
24};
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::Arc;
27use u_nesting_core::gdrr::{
28    GdrrConfig, GdrrProblem, GdrrResult, GdrrRunner, GdrrSolution, RecreateResult, RecreateType,
29    RuinResult, RuinType, RuinedItem,
30};
31use u_nesting_core::geometry::{Boundary, Geometry};
32use u_nesting_core::solver::Config;
33use u_nesting_core::timing::Timer;
34use u_nesting_core::{Placement, SolveResult};
35
36use crate::placement_utils::{expand_nfp, shrink_ifp, InstanceInfo};
37use rand::prelude::*;
38
39/// A placed item in the GDRR solution.
40#[derive(Debug, Clone)]
41pub struct PlacedItem {
42    /// Instance index.
43    pub instance_idx: usize,
44    /// X position.
45    pub x: f64,
46    /// Y position.
47    pub y: f64,
48    /// Rotation angle in radians.
49    pub rotation: f64,
50    /// Whether the item was placed mirrored (`allow_flip` support).
51    pub mirrored: bool,
52    /// Placement score (lower = better, based on position).
53    pub score: f64,
54}
55
56/// GDRR solution for 2D nesting.
57#[derive(Debug, Clone)]
58pub struct GdrrNestingSolution {
59    /// Placed items.
60    pub placed: Vec<PlacedItem>,
61    /// Unplaced instance indices.
62    pub unplaced: Vec<usize>,
63    /// Total number of instances.
64    pub total_instances: usize,
65    /// Total placed area.
66    pub placed_area: f64,
67    /// Boundary area.
68    pub boundary_area: f64,
69    /// Maximum Y coordinate used (strip height).
70    pub max_y: f64,
71}
72
73impl GdrrNestingSolution {
74    /// Create a new empty solution.
75    pub fn new(total_instances: usize, boundary_area: f64) -> Self {
76        Self {
77            placed: Vec::new(),
78            unplaced: (0..total_instances).collect(),
79            total_instances,
80            placed_area: 0.0,
81            boundary_area,
82            max_y: 0.0,
83        }
84    }
85}
86
87impl GdrrSolution for GdrrNestingSolution {
88    fn fitness(&self) -> f64 {
89        // Fitness combines:
90        // 1. Penalty for unplaced items (high weight)
91        // 2. Inverse of utilization (lower is better)
92        // 3. Strip height minimization
93        let unplaced_penalty = self.unplaced.len() as f64 * 1000.0;
94        let utilization_penalty = if self.placed_area > 0.0 {
95            1.0 - (self.placed_area / self.boundary_area)
96        } else {
97            1.0
98        };
99        let height_penalty = self.max_y / 1000.0; // Normalized height
100
101        unplaced_penalty + utilization_penalty + height_penalty
102    }
103
104    fn placed_count(&self) -> usize {
105        self.placed.len()
106    }
107
108    fn total_count(&self) -> usize {
109        self.total_instances
110    }
111
112    fn utilization(&self) -> f64 {
113        if self.boundary_area > 0.0 {
114            self.placed_area / self.boundary_area
115        } else {
116            0.0
117        }
118    }
119
120    fn fits_goal(&self, goal: f64) -> bool {
121        // Goal represents target utilization or strip height
122        self.fitness() <= goal
123    }
124}
125
126/// GDRR problem definition for 2D nesting.
127pub struct GdrrNestingProblem {
128    /// Input geometries.
129    geometries: Vec<Geometry2D>,
130    /// Boundary container.
131    boundary: Boundary2D,
132    /// Solver configuration.
133    config: Config,
134    /// Instance mapping.
135    instances: Vec<InstanceInfo>,
136    /// Available rotation angles per geometry.
137    rotation_angles: Vec<Vec<f64>>,
138    /// Geometry areas.
139    geometry_areas: Vec<f64>,
140    /// Cancellation flag.
141    cancelled: Arc<AtomicBool>,
142    /// Start time for timeout checking.
143    start_time: Timer,
144    /// Time limit in milliseconds.
145    time_limit_ms: u64,
146}
147
148impl GdrrNestingProblem {
149    /// Creates a new GDRR nesting problem.
150    pub fn new(
151        geometries: Vec<Geometry2D>,
152        boundary: Boundary2D,
153        config: Config,
154        cancelled: Arc<AtomicBool>,
155        time_limit_ms: u64,
156    ) -> Self {
157        // Build instance mapping
158        let mut instances = Vec::new();
159        let mut rotation_angles = Vec::new();
160        let mut geometry_areas = Vec::new();
161
162        for (geom_idx, geom) in geometries.iter().enumerate() {
163            // Get rotation angles
164            let angles = geom.rotations();
165            let angles = if angles.is_empty() { vec![0.0] } else { angles };
166            rotation_angles.push(angles);
167
168            // Compute area
169            let area = geom.measure();
170            geometry_areas.push(area);
171
172            // Create instances
173            for instance_num in 0..geom.quantity() {
174                instances.push(InstanceInfo {
175                    geometry_idx: geom_idx,
176                    instance_num,
177                });
178            }
179        }
180
181        Self {
182            geometries,
183            boundary,
184            config,
185            instances,
186            rotation_angles,
187            geometry_areas,
188            cancelled,
189            start_time: Timer::now(),
190            time_limit_ms,
191        }
192    }
193
194    /// Check if timeout has been reached.
195    fn is_timed_out(&self) -> bool {
196        if self.time_limit_ms == 0 {
197            return false;
198        }
199        self.start_time.elapsed_ms() >= self.time_limit_ms
200    }
201
202    /// Returns the total number of instances.
203    pub fn num_instances(&self) -> usize {
204        self.instances.len()
205    }
206
207    /// Get boundary polygon with margin.
208    fn get_boundary_polygon_with_margin(&self, margin: f64) -> Vec<(f64, f64)> {
209        let (min, max) = self.boundary.aabb();
210        vec![
211            (min[0] + margin, min[1] + margin),
212            (max[0] - margin, min[1] + margin),
213            (max[0] - margin, max[1] - margin),
214            (min[0] + margin, max[1] - margin),
215        ]
216    }
217
218    /// Compute sample step for grid search.
219    fn compute_sample_step(&self) -> f64 {
220        let (min, max) = self.boundary.aabb();
221        let width = max[0] - min[0];
222        (width / 100.0).max(1.0)
223    }
224
225    /// Try to place an item at the best position using NFP.
226    fn try_place_item(
227        &self,
228        instance_idx: usize,
229        placed_geometries: &[PlacedGeometry],
230        boundary_polygon: &[(f64, f64)],
231        sample_step: f64,
232    ) -> Option<PlacedItem> {
233        let info = &self.instances[instance_idx];
234        let geom = &self.geometries[info.geometry_idx];
235        let angles = &self.rotation_angles[info.geometry_idx];
236        // Mirror candidates (`allow_flip` support) — same pattern as
237        // `nester.rs`'s `mirror_candidates` helper.
238        let mirror_candidates: &[bool] = if geom.allow_flip() {
239            &[false, true]
240        } else {
241            &[false]
242        };
243
244        let mut best_placement: Option<PlacedItem> = None;
245        let mut best_y = f64::MAX;
246
247        for &rotation in angles {
248            for &mirror in mirror_candidates {
249                // Compute IFP
250                let ifp = match compute_ifp_with_margin_and_mirror(
251                    boundary_polygon,
252                    geom,
253                    rotation,
254                    0.0,
255                    mirror,
256                ) {
257                    Ok(ifp) => ifp,
258                    Err(_) => continue,
259                };
260
261                if ifp.is_empty() {
262                    continue;
263                }
264
265                // Compute NFPs with placed geometries
266                let spacing = self.config.spacing;
267                let mut nfps: Vec<Nfp> = Vec::new();
268
269                for pg in placed_geometries {
270                    // Already-mirrored (if applicable) real-world polygon —
271                    // do NOT mirror it again below, `mirror_stationary=false` always.
272                    let placed_exterior = pg.translated_exterior();
273                    let placed_geom = Geometry2D::new(format!("_placed_{}", pg.geometry.id()))
274                        .with_polygon(placed_exterior);
275
276                    if let Ok(nfp) =
277                        compute_nfp_mirrored(&placed_geom, geom, rotation, false, mirror)
278                    {
279                        let expanded = expand_nfp(&nfp, spacing);
280                        nfps.push(expanded);
281                    }
282                }
283
284                // Shrink IFP by spacing
285                let ifp_shrunk = shrink_ifp(&ifp, spacing);
286
287                // Find bottom-left placement
288                // IFP returns positions where the geometry's origin should be placed.
289                // Clamp to ensure placement keeps geometry within boundary.
290                let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
291                if let Some((x, y)) =
292                    find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step)
293                {
294                    // Clamp position to keep geometry within boundary
295                    // (mirror-aware — an unmirrored AABB has the wrong local
296                    // extents for a mirrored candidate, see `aabb_at_rotation_mirrored`).
297                    let geom_aabb = geom.aabb_at_rotation_mirrored(rotation, mirror);
298                    let boundary_aabb = self.boundary.aabb();
299
300                    if let Some((clamped_x, clamped_y)) =
301                        clamp_placement_to_boundary(x, y, geom_aabb, boundary_aabb)
302                    {
303                        // Only verify overlap if clamping changed the position
304                        // The original NFP-found position is already collision-free by definition
305                        let was_clamped =
306                            (clamped_x - x).abs() > 1e-6 || (clamped_y - y).abs() > 1e-6;
307                        if was_clamped {
308                            // Verify no actual polygon overlap using SAT
309                            if !verify_no_overlap_mirrored(
310                                geom,
311                                (clamped_x, clamped_y),
312                                rotation,
313                                mirror,
314                                placed_geometries,
315                            ) {
316                                continue; // Skip - clamped position would cause overlap
317                            }
318                        }
319
320                        if clamped_y < best_y {
321                            best_y = clamped_y;
322                            best_placement = Some(PlacedItem {
323                                instance_idx,
324                                x: clamped_x,
325                                y: clamped_y,
326                                rotation,
327                                mirrored: mirror,
328                                score: clamped_y, // Score based on Y position
329                            });
330                        }
331                    }
332                }
333            }
334        }
335
336        best_placement
337    }
338
339    /// Place items using BLF heuristic.
340    fn place_items_blf(&self, items: &[usize], solution: &mut GdrrNestingSolution) {
341        let margin = self.config.margin;
342        let boundary_polygon = self.get_boundary_polygon_with_margin(margin);
343        let sample_step = self.compute_sample_step();
344
345        // Build placed geometries from current solution
346        let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
347        for item in &solution.placed {
348            let info = &self.instances[item.instance_idx];
349            let geom = &self.geometries[info.geometry_idx];
350            placed_geometries.push(PlacedGeometry {
351                geometry: geom.clone(),
352                position: (item.x, item.y),
353                rotation: item.rotation,
354                mirrored: item.mirrored,
355            });
356        }
357
358        // Sort items by area (largest first)
359        let mut sorted_items = items.to_vec();
360        sorted_items.sort_by(|&a, &b| {
361            let area_a = self.geometry_areas[self.instances[a].geometry_idx];
362            let area_b = self.geometry_areas[self.instances[b].geometry_idx];
363            area_b
364                .partial_cmp(&area_a)
365                .unwrap_or(std::cmp::Ordering::Equal)
366        });
367
368        for &instance_idx in &sorted_items {
369            // Check cancellation and timeout
370            if self.cancelled.load(Ordering::Relaxed) || self.is_timed_out() {
371                break;
372            }
373
374            if let Some(placement) = self.try_place_item(
375                instance_idx,
376                &placed_geometries,
377                &boundary_polygon,
378                sample_step,
379            ) {
380                // Update solution
381                let info = &self.instances[instance_idx];
382                let area = self.geometry_areas[info.geometry_idx];
383
384                solution.placed_area += area;
385                solution.max_y = solution.max_y.max(placement.y);
386
387                // Add to placed geometries for next iterations
388                let geom = &self.geometries[info.geometry_idx];
389                placed_geometries.push(PlacedGeometry {
390                    geometry: geom.clone(),
391                    position: (placement.x, placement.y),
392                    rotation: placement.rotation,
393                    mirrored: placement.mirrored,
394                });
395
396                solution.placed.push(placement);
397                solution.unplaced.retain(|&idx| idx != instance_idx);
398            }
399        }
400    }
401}
402
403impl GdrrProblem for GdrrNestingProblem {
404    type Solution = GdrrNestingSolution;
405
406    fn create_initial_solution(&mut self) -> GdrrNestingSolution {
407        let boundary_area = self.boundary.measure();
408        let mut solution = GdrrNestingSolution::new(self.instances.len(), boundary_area);
409
410        // Place all items using BLF
411        let all_items: Vec<usize> = (0..self.instances.len()).collect();
412        self.place_items_blf(&all_items, &mut solution);
413
414        solution
415    }
416
417    fn clone_solution(&self, solution: &GdrrNestingSolution) -> GdrrNestingSolution {
418        solution.clone()
419    }
420
421    fn ruin_random(
422        &mut self,
423        solution: &mut GdrrNestingSolution,
424        ratio: f64,
425        rng: &mut rand::rngs::StdRng,
426    ) -> RuinResult {
427        let num_to_remove = ((solution.placed.len() as f64 * ratio).ceil() as usize).max(1);
428        let mut removed_items = Vec::new();
429
430        if solution.placed.is_empty() {
431            return RuinResult {
432                removed_items,
433                ruin_type: RuinType::Random,
434            };
435        }
436
437        // Randomly select items to remove
438        let mut indices: Vec<usize> = (0..solution.placed.len()).collect();
439        indices.shuffle(rng);
440
441        for &idx in indices.iter().take(num_to_remove) {
442            let item = &solution.placed[idx];
443            let info = &self.instances[item.instance_idx];
444
445            removed_items.push(RuinedItem {
446                index: item.instance_idx,
447                geometry_id: self.geometries[info.geometry_idx].id().to_string(),
448                position: vec![item.x, item.y],
449                rotation: item.rotation,
450                score: item.score,
451            });
452        }
453
454        // Remove items from solution
455        let removed_instance_indices: Vec<usize> = removed_items.iter().map(|r| r.index).collect();
456
457        for idx in &removed_instance_indices {
458            if let Some(pos) = solution.placed.iter().position(|p| p.instance_idx == *idx) {
459                let item = solution.placed.remove(pos);
460                let info = &self.instances[item.instance_idx];
461                solution.placed_area -= self.geometry_areas[info.geometry_idx];
462                solution.unplaced.push(item.instance_idx);
463            }
464        }
465
466        // Recalculate max_y
467        solution.max_y = solution.placed.iter().map(|p| p.y).fold(0.0, f64::max);
468
469        RuinResult {
470            removed_items,
471            ruin_type: RuinType::Random,
472        }
473    }
474
475    fn ruin_cluster(
476        &mut self,
477        solution: &mut GdrrNestingSolution,
478        ratio: f64,
479        rng: &mut rand::rngs::StdRng,
480    ) -> RuinResult {
481        let num_to_remove = ((solution.placed.len() as f64 * ratio).ceil() as usize).max(1);
482        let mut removed_items = Vec::new();
483
484        if solution.placed.is_empty() {
485            return RuinResult {
486                removed_items,
487                ruin_type: RuinType::Cluster,
488            };
489        }
490
491        // Select a random seed item
492        let seed_idx = rng.random_range(0..solution.placed.len());
493        let seed = &solution.placed[seed_idx];
494        let seed_x = seed.x;
495        let seed_y = seed.y;
496
497        // Sort items by distance to seed
498        let mut items_with_distance: Vec<(usize, f64)> = solution
499            .placed
500            .iter()
501            .enumerate()
502            .map(|(idx, item)| {
503                let dx = item.x - seed_x;
504                let dy = item.y - seed_y;
505                (idx, (dx * dx + dy * dy).sqrt())
506            })
507            .collect();
508
509        items_with_distance
510            .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
511
512        // Remove closest items (including seed)
513        for (idx, _) in items_with_distance.iter().take(num_to_remove) {
514            let item = &solution.placed[*idx];
515            let info = &self.instances[item.instance_idx];
516
517            removed_items.push(RuinedItem {
518                index: item.instance_idx,
519                geometry_id: self.geometries[info.geometry_idx].id().to_string(),
520                position: vec![item.x, item.y],
521                rotation: item.rotation,
522                score: item.score,
523            });
524        }
525
526        // Remove items from solution
527        let removed_instance_indices: Vec<usize> = removed_items.iter().map(|r| r.index).collect();
528
529        for idx in &removed_instance_indices {
530            if let Some(pos) = solution.placed.iter().position(|p| p.instance_idx == *idx) {
531                let item = solution.placed.remove(pos);
532                let info = &self.instances[item.instance_idx];
533                solution.placed_area -= self.geometry_areas[info.geometry_idx];
534                solution.unplaced.push(item.instance_idx);
535            }
536        }
537
538        // Recalculate max_y
539        solution.max_y = solution.placed.iter().map(|p| p.y).fold(0.0, f64::max);
540
541        RuinResult {
542            removed_items,
543            ruin_type: RuinType::Cluster,
544        }
545    }
546
547    fn ruin_worst(
548        &mut self,
549        solution: &mut GdrrNestingSolution,
550        ratio: f64,
551        _rng: &mut rand::rngs::StdRng,
552    ) -> RuinResult {
553        let num_to_remove = ((solution.placed.len() as f64 * ratio).ceil() as usize).max(1);
554        let mut removed_items = Vec::new();
555
556        if solution.placed.is_empty() {
557            return RuinResult {
558                removed_items,
559                ruin_type: RuinType::Worst,
560            };
561        }
562
563        // Sort by score (higher = worse, we use Y position as score)
564        let mut items_with_score: Vec<(usize, f64)> = solution
565            .placed
566            .iter()
567            .enumerate()
568            .map(|(idx, item)| (idx, item.score))
569            .collect();
570
571        items_with_score.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
572
573        // Remove worst items
574        for (idx, _) in items_with_score.iter().take(num_to_remove) {
575            let item = &solution.placed[*idx];
576            let info = &self.instances[item.instance_idx];
577
578            removed_items.push(RuinedItem {
579                index: item.instance_idx,
580                geometry_id: self.geometries[info.geometry_idx].id().to_string(),
581                position: vec![item.x, item.y],
582                rotation: item.rotation,
583                score: item.score,
584            });
585        }
586
587        // Remove items from solution
588        let removed_instance_indices: Vec<usize> = removed_items.iter().map(|r| r.index).collect();
589
590        for idx in &removed_instance_indices {
591            if let Some(pos) = solution.placed.iter().position(|p| p.instance_idx == *idx) {
592                let item = solution.placed.remove(pos);
593                let info = &self.instances[item.instance_idx];
594                solution.placed_area -= self.geometry_areas[info.geometry_idx];
595                solution.unplaced.push(item.instance_idx);
596            }
597        }
598
599        // Recalculate max_y
600        solution.max_y = solution.placed.iter().map(|p| p.y).fold(0.0, f64::max);
601
602        RuinResult {
603            removed_items,
604            ruin_type: RuinType::Worst,
605        }
606    }
607
608    fn recreate_best_fit(
609        &mut self,
610        solution: &mut GdrrNestingSolution,
611        _ruined: &RuinResult,
612    ) -> RecreateResult {
613        let items_to_place = solution.unplaced.clone();
614        let initial_placed = solution.placed.len();
615
616        self.place_items_blf(&items_to_place, solution);
617
618        RecreateResult {
619            placed_count: solution.placed.len() - initial_placed,
620            unplaced_count: solution.unplaced.len(),
621            recreate_type: RecreateType::BestFit,
622        }
623    }
624
625    fn recreate_blf(
626        &mut self,
627        solution: &mut GdrrNestingSolution,
628        _ruined: &RuinResult,
629    ) -> RecreateResult {
630        let items_to_place = solution.unplaced.clone();
631        let initial_placed = solution.placed.len();
632
633        self.place_items_blf(&items_to_place, solution);
634
635        RecreateResult {
636            placed_count: solution.placed.len() - initial_placed,
637            unplaced_count: solution.unplaced.len(),
638            recreate_type: RecreateType::BottomLeftFill,
639        }
640    }
641
642    fn recreate_nfp(
643        &mut self,
644        solution: &mut GdrrNestingSolution,
645        _ruined: &RuinResult,
646    ) -> RecreateResult {
647        // NFP-guided placement (same as BLF but with potential for optimization)
648        let items_to_place = solution.unplaced.clone();
649        let initial_placed = solution.placed.len();
650
651        self.place_items_blf(&items_to_place, solution);
652
653        RecreateResult {
654            placed_count: solution.placed.len() - initial_placed,
655            unplaced_count: solution.unplaced.len(),
656            recreate_type: RecreateType::NfpGuided,
657        }
658    }
659
660    fn placement_score(&self, solution: &GdrrNestingSolution, item_index: usize) -> f64 {
661        solution
662            .placed
663            .iter()
664            .find(|p| p.instance_idx == item_index)
665            .map(|p| p.score)
666            .unwrap_or(f64::MAX)
667    }
668
669    fn get_neighbors(
670        &self,
671        solution: &GdrrNestingSolution,
672        item_index: usize,
673        radius: f64,
674    ) -> Vec<usize> {
675        let item = match solution
676            .placed
677            .iter()
678            .find(|p| p.instance_idx == item_index)
679        {
680            Some(i) => i,
681            None => return vec![],
682        };
683
684        solution
685            .placed
686            .iter()
687            .filter(|p| {
688                if p.instance_idx == item_index {
689                    return false;
690                }
691                let dx = p.x - item.x;
692                let dy = p.y - item.y;
693                (dx * dx + dy * dy).sqrt() <= radius
694            })
695            .map(|p| p.instance_idx)
696            .collect()
697    }
698}
699
700/// Run GDRR nesting optimization.
701pub fn run_gdrr_nesting(
702    geometries: &[Geometry2D],
703    boundary: &Boundary2D,
704    config: &Config,
705    gdrr_config: &GdrrConfig,
706    cancelled: Arc<AtomicBool>,
707) -> SolveResult<f64> {
708    let mut problem = GdrrNestingProblem::new(
709        geometries.to_vec(),
710        boundary.clone(),
711        config.clone(),
712        cancelled,
713        gdrr_config.time_limit_ms,
714    );
715
716    let runner = GdrrRunner::new(gdrr_config.clone());
717    let gdrr_result: GdrrResult<GdrrNestingSolution> = runner.run(&mut problem, |_progress| {
718        // Progress callback - can be used for logging
719    });
720
721    // Convert GDRR solution to SolveResult
722    let mut result = SolveResult::new();
723
724    for item in &gdrr_result.best_solution.placed {
725        let info = &problem.instances[item.instance_idx];
726        let geom = &problem.geometries[info.geometry_idx];
727
728        result.placements.push(
729            Placement::new_2d(
730                geom.id().to_string(),
731                info.instance_num,
732                item.x,
733                item.y,
734                item.rotation,
735            )
736            .with_mirrored(item.mirrored),
737        );
738    }
739
740    result.boundaries_used = if result.placements.is_empty() { 0 } else { 1 };
741    result.utilization = gdrr_result.best_solution.utilization();
742    result.computation_time_ms = gdrr_result.elapsed_ms;
743    result.iterations = Some(gdrr_result.iterations as u64);
744    result.best_fitness = Some(gdrr_result.best_fitness);
745    result.strategy = Some("GDRR".to_string());
746
747    result
748}
749
750#[cfg(test)]
751mod tests {
752    use super::*;
753
754    fn create_test_geometries() -> Vec<Geometry2D> {
755        vec![
756            Geometry2D::rectangle("rect1", 50.0, 30.0).with_quantity(3),
757            Geometry2D::rectangle("rect2", 40.0, 40.0).with_quantity(2),
758            Geometry2D::rectangle("rect3", 60.0, 20.0).with_quantity(2),
759        ]
760    }
761
762    fn create_test_boundary() -> Boundary2D {
763        Boundary2D::rectangle(300.0, 200.0)
764    }
765
766    #[test]
767    fn test_gdrr_nesting_problem_creation() {
768        let geometries = create_test_geometries();
769        let boundary = create_test_boundary();
770        let config = Config::default();
771        let cancelled = Arc::new(AtomicBool::new(false));
772
773        let problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
774
775        assert_eq!(problem.num_instances(), 7); // 3 + 2 + 2
776    }
777
778    #[test]
779    fn test_gdrr_nesting_initial_solution() {
780        let geometries = create_test_geometries();
781        let boundary = create_test_boundary();
782        let config = Config::default();
783        let cancelled = Arc::new(AtomicBool::new(false));
784
785        let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
786        let solution = problem.create_initial_solution();
787
788        assert!(!solution.placed.is_empty());
789        assert!(solution.placed_area > 0.0);
790    }
791
792    #[test]
793    fn test_gdrr_nesting_solution_fitness() {
794        let solution = GdrrNestingSolution {
795            placed: vec![
796                PlacedItem {
797                    instance_idx: 0,
798                    x: 10.0,
799                    y: 10.0,
800                    rotation: 0.0,
801                    mirrored: false,
802                    score: 10.0,
803                },
804                PlacedItem {
805                    instance_idx: 1,
806                    x: 60.0,
807                    y: 10.0,
808                    rotation: 0.0,
809                    mirrored: false,
810                    score: 10.0,
811                },
812            ],
813            unplaced: vec![2],
814            total_instances: 3,
815            placed_area: 3000.0,
816            boundary_area: 60000.0,
817            max_y: 50.0,
818        };
819
820        let fitness = solution.fitness();
821        assert!(fitness > 0.0);
822        // 1 unplaced item = 1000 penalty + utilization penalty + height penalty
823        assert!(fitness >= 1000.0);
824    }
825
826    #[test]
827    fn test_gdrr_nesting_ruin_random() {
828        use rand::SeedableRng;
829
830        let geometries = create_test_geometries();
831        let boundary = create_test_boundary();
832        let config = Config::default();
833        let cancelled = Arc::new(AtomicBool::new(false));
834
835        let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
836        let mut solution = problem.create_initial_solution();
837
838        let initial_placed = solution.placed.len();
839        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
840
841        let result = problem.ruin_random(&mut solution, 0.3, &mut rng);
842
843        assert!(!result.removed_items.is_empty());
844        assert_eq!(result.ruin_type, RuinType::Random);
845        assert!(solution.placed.len() < initial_placed);
846    }
847
848    #[test]
849    fn test_gdrr_nesting_ruin_cluster() {
850        use rand::SeedableRng;
851
852        let geometries = create_test_geometries();
853        let boundary = create_test_boundary();
854        let config = Config::default();
855        let cancelled = Arc::new(AtomicBool::new(false));
856
857        let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
858        let mut solution = problem.create_initial_solution();
859
860        let initial_placed = solution.placed.len();
861        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
862
863        let result = problem.ruin_cluster(&mut solution, 0.3, &mut rng);
864
865        assert!(!result.removed_items.is_empty());
866        assert_eq!(result.ruin_type, RuinType::Cluster);
867        assert!(solution.placed.len() < initial_placed);
868    }
869
870    #[test]
871    fn test_gdrr_nesting_ruin_worst() {
872        use rand::SeedableRng;
873
874        let geometries = create_test_geometries();
875        let boundary = create_test_boundary();
876        let config = Config::default();
877        let cancelled = Arc::new(AtomicBool::new(false));
878
879        let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
880        let mut solution = problem.create_initial_solution();
881
882        let initial_placed = solution.placed.len();
883        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
884
885        let result = problem.ruin_worst(&mut solution, 0.3, &mut rng);
886
887        assert!(!result.removed_items.is_empty());
888        assert_eq!(result.ruin_type, RuinType::Worst);
889        assert!(solution.placed.len() < initial_placed);
890    }
891
892    #[test]
893    fn test_gdrr_nesting_recreate() {
894        use rand::SeedableRng;
895
896        let geometries = create_test_geometries();
897        let boundary = create_test_boundary();
898        let config = Config::default();
899        let cancelled = Arc::new(AtomicBool::new(false));
900
901        let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
902        let mut solution = problem.create_initial_solution();
903
904        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
905
906        // Ruin some items
907        let ruin_result = problem.ruin_random(&mut solution, 0.5, &mut rng);
908        let after_ruin_placed = solution.placed.len();
909
910        // Recreate
911        let recreate_result = problem.recreate_best_fit(&mut solution, &ruin_result);
912
913        assert!(recreate_result.placed_count > 0 || after_ruin_placed == solution.placed.len());
914        assert_eq!(recreate_result.recreate_type, RecreateType::BestFit);
915    }
916
917    #[test]
918    fn test_run_gdrr_nesting() {
919        let geometries = create_test_geometries();
920        let boundary = create_test_boundary();
921        let config = Config::default();
922        let gdrr_config = GdrrConfig::new()
923            .with_max_iterations(50)
924            .with_time_limit_ms(5000);
925        let cancelled = Arc::new(AtomicBool::new(false));
926
927        let result = run_gdrr_nesting(&geometries, &boundary, &config, &gdrr_config, cancelled);
928
929        assert!(!result.placements.is_empty());
930        assert!(result.utilization > 0.0);
931    }
932
933    #[test]
934    fn test_gdrr_nesting_full_cycle() {
935        let geometries = create_test_geometries();
936        let boundary = create_test_boundary();
937        let config = Config::default();
938        let cancelled = Arc::new(AtomicBool::new(false));
939
940        let mut problem = GdrrNestingProblem::new(geometries, boundary, config, cancelled, 60000);
941
942        let gdrr_config = GdrrConfig::new().with_max_iterations(10).with_seed(42);
943
944        let runner = GdrrRunner::new(gdrr_config);
945        let result: GdrrResult<GdrrNestingSolution> = runner.run(&mut problem, |progress| {
946            assert!(progress.iteration <= 10);
947        });
948
949        assert!(result.iterations <= 10);
950        assert!(!result.best_solution.placed.is_empty());
951    }
952
953    /// Chiral L-shape — see `nfp.rs`'s `chiral_l` fixture for why this
954    /// specific shape (asymmetric width/height/notch, no reflection symmetry).
955    fn chiral_l(id: &str) -> Geometry2D {
956        Geometry2D::l_shape(id, 30.0, 20.0, 20.0, 10.0)
957    }
958
959    fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
960        for i in 0..a.len() {
961            let (a1, a2) = (a[i], a[(i + 1) % a.len()]);
962            for j in 0..b.len() {
963                let (b1, b2) = (b[j], b[(j + 1) % b.len()]);
964                if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
965                    return true;
966                }
967            }
968        }
969        false
970    }
971
972    /// `allow_flip`/mirroring, GDRR strategy. `try_place_item` (and the rest
973    /// of GDRR) calls no `.validate()`, so calling it directly exercises the
974    /// mirror candidate deterministically — same rationale as GA/SA/BRKGA;
975    /// useful even now that the public gate is open (Phase 4), since GDRR's
976    /// own public path is randomized.
977    #[test]
978    fn test_gdrr_try_place_item_mirror_no_overlap() {
979        let geometries = vec![chiral_l("L").with_flip(true).with_quantity(2)];
980        let boundary = Boundary2D::rectangle(65.0, 45.0);
981        let config = Config::default().with_spacing(1.0);
982        let problem = GdrrNestingProblem::new(
983            geometries.clone(),
984            boundary,
985            config,
986            Arc::new(AtomicBool::new(false)),
987            60000,
988        );
989
990        let margin = problem.config.margin;
991        let boundary_polygon = problem.get_boundary_polygon_with_margin(margin);
992        let sample_step = problem.compute_sample_step();
993
994        // Place instance 0 first (no other placed pieces to avoid).
995        let placement0 = problem
996            .try_place_item(0, &[], &boundary_polygon, sample_step)
997            .expect("first piece must fit");
998
999        // Place instance 1 avoiding instance 0.
1000        let placed_geometries = vec![PlacedGeometry {
1001            geometry: geometries[0].clone(),
1002            position: (placement0.x, placement0.y),
1003            rotation: placement0.rotation,
1004            mirrored: placement0.mirrored,
1005        }];
1006        let placement1 = problem
1007            .try_place_item(1, &placed_geometries, &boundary_polygon, sample_step)
1008            .expect("second piece must fit avoiding the first");
1009
1010        // At least one of the two should end up mirrored in this tight,
1011        // symmetric-instance scenario — otherwise this test isn't actually
1012        // exercising the mirror path, just confirming allow_flip doesn't
1013        // break anything (best_y ties break toward whichever candidate is
1014        // evaluated first, so both false is possible but both being false
1015        // AND non-overlapping AND placed would mean mirroring never even
1016        // got a chance to matter here — assert overlap-freedom instead,
1017        // which holds regardless of which orientation won).
1018        let poly0 = PlacedGeometry {
1019            geometry: geometries[0].clone(),
1020            position: (placement0.x, placement0.y),
1021            rotation: placement0.rotation,
1022            mirrored: placement0.mirrored,
1023        }
1024        .translated_exterior();
1025        let poly1 = PlacedGeometry {
1026            geometry: geometries[0].clone(),
1027            position: (placement1.x, placement1.y),
1028            rotation: placement1.rotation,
1029            mirrored: placement1.mirrored,
1030        }
1031        .translated_exterior();
1032        assert!(
1033            !polygons_overlap(&poly0, &poly1),
1034            "instance 0 (mirrored={}) and instance 1 (mirrored={}) must not overlap",
1035            placement0.mirrored,
1036            placement1.mirrored
1037        );
1038    }
1039
1040    #[test]
1041    fn test_gdrr_try_place_item_mirror_ignored_without_allow_flip() {
1042        let geometries = vec![chiral_l("L").with_quantity(1)];
1043        let boundary = Boundary2D::rectangle(65.0, 45.0);
1044        let problem = GdrrNestingProblem::new(
1045            geometries,
1046            boundary,
1047            Config::default(),
1048            Arc::new(AtomicBool::new(false)),
1049            60000,
1050        );
1051
1052        let margin = problem.config.margin;
1053        let boundary_polygon = problem.get_boundary_polygon_with_margin(margin);
1054        let sample_step = problem.compute_sample_step();
1055
1056        let placement = problem
1057            .try_place_item(0, &[], &boundary_polygon, sample_step)
1058            .expect("piece must fit");
1059        assert!(
1060            !placement.mirrored,
1061            "allow_flip=false must suppress mirroring"
1062        );
1063    }
1064}