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