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