u_nesting_d2/
ga_nesting.rs

1//! Genetic Algorithm based 2D nesting optimization.
2//!
3//! This module provides GA-based optimization for 2D nesting problems,
4//! using the permutation chromosome representation and NFP-guided decoding.
5
6use crate::boundary::Boundary2D;
7use crate::clamp_placement_to_boundary;
8use crate::geometry::Geometry2D;
9use crate::nfp::{compute_ifp, compute_nfp, find_bottom_left_placement, Nfp, PlacedGeometry};
10use rand::prelude::*;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::Arc;
13use u_nesting_core::ga::{GaConfig, GaProblem, GaProgress, GaRunner, Individual};
14use u_nesting_core::geometry::{Boundary, Geometry};
15use u_nesting_core::solver::{Config, ProgressCallback, ProgressInfo};
16use u_nesting_core::{Placement, SolveResult};
17
18/// Nesting chromosome representing a placement order and rotations.
19#[derive(Debug, Clone)]
20pub struct NestingChromosome {
21    /// Permutation of geometry indices (placement order).
22    pub order: Vec<usize>,
23    /// Rotation index for each geometry instance.
24    pub rotations: Vec<usize>,
25    /// Cached fitness value.
26    fitness: f64,
27    /// Number of placed pieces (for fitness calculation).
28    placed_count: usize,
29    /// Total instances count.
30    total_count: usize,
31}
32
33impl NestingChromosome {
34    /// Creates a new chromosome for the given number of instances and rotation options.
35    pub fn new(num_instances: usize, _rotation_options: usize) -> Self {
36        Self {
37            order: (0..num_instances).collect(),
38            rotations: vec![0; num_instances],
39            fitness: f64::NEG_INFINITY,
40            placed_count: 0,
41            total_count: num_instances,
42        }
43    }
44
45    /// Creates a random chromosome.
46    pub fn random_with_options<R: Rng>(
47        num_instances: usize,
48        rotation_options: usize,
49        rng: &mut R,
50    ) -> Self {
51        let mut order: Vec<usize> = (0..num_instances).collect();
52        order.shuffle(rng);
53
54        let rotations: Vec<usize> = (0..num_instances)
55            .map(|_| rng.gen_range(0..rotation_options.max(1)))
56            .collect();
57
58        Self {
59            order,
60            rotations,
61            fitness: f64::NEG_INFINITY,
62            placed_count: 0,
63            total_count: num_instances,
64        }
65    }
66
67    /// Sets the fitness value.
68    pub fn set_fitness(&mut self, fitness: f64, placed_count: usize) {
69        self.fitness = fitness;
70        self.placed_count = placed_count;
71    }
72
73    /// Order crossover (OX1) for permutation genes.
74    pub fn order_crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
75        let n = self.order.len();
76        if n < 2 {
77            return self.clone();
78        }
79
80        // Select two crossover points
81        let (mut p1, mut p2) = (rng.gen_range(0..n), rng.gen_range(0..n));
82        if p1 > p2 {
83            std::mem::swap(&mut p1, &mut p2);
84        }
85
86        // Copy segment from parent1
87        let mut child_order = vec![usize::MAX; n];
88        let mut used = vec![false; n];
89
90        for i in p1..=p2 {
91            child_order[i] = self.order[i];
92            used[self.order[i]] = true;
93        }
94
95        // Fill remaining from parent2
96        let mut j = (p2 + 1) % n;
97        for i in 0..n {
98            let idx = (p2 + 1 + i) % n;
99            if child_order[idx] == usize::MAX {
100                while used[other.order[j]] {
101                    j = (j + 1) % n;
102                }
103                child_order[idx] = other.order[j];
104                used[other.order[j]] = true;
105                j = (j + 1) % n;
106            }
107        }
108
109        // Crossover rotations (uniform)
110        let rotations: Vec<usize> = self
111            .rotations
112            .iter()
113            .zip(&other.rotations)
114            .map(|(a, b)| if rng.gen() { *a } else { *b })
115            .collect();
116
117        Self {
118            order: child_order,
119            rotations,
120            fitness: f64::NEG_INFINITY,
121            placed_count: 0,
122            total_count: self.total_count,
123        }
124    }
125
126    /// Swap mutation for order genes.
127    pub fn swap_mutate<R: Rng>(&mut self, rng: &mut R) {
128        if self.order.len() < 2 {
129            return;
130        }
131
132        let i = rng.gen_range(0..self.order.len());
133        let j = rng.gen_range(0..self.order.len());
134        self.order.swap(i, j);
135        self.fitness = f64::NEG_INFINITY;
136    }
137
138    /// Rotation mutation.
139    pub fn rotation_mutate<R: Rng>(&mut self, rotation_options: usize, rng: &mut R) {
140        if self.rotations.is_empty() || rotation_options <= 1 {
141            return;
142        }
143
144        let idx = rng.gen_range(0..self.rotations.len());
145        self.rotations[idx] = rng.gen_range(0..rotation_options);
146        self.fitness = f64::NEG_INFINITY;
147    }
148
149    /// Inversion mutation (reverses a segment).
150    pub fn inversion_mutate<R: Rng>(&mut self, rng: &mut R) {
151        let n = self.order.len();
152        if n < 2 {
153            return;
154        }
155
156        let (mut p1, mut p2) = (rng.gen_range(0..n), rng.gen_range(0..n));
157        if p1 > p2 {
158            std::mem::swap(&mut p1, &mut p2);
159        }
160
161        self.order[p1..=p2].reverse();
162        self.fitness = f64::NEG_INFINITY;
163    }
164}
165
166impl Individual for NestingChromosome {
167    type Fitness = f64;
168
169    fn fitness(&self) -> f64 {
170        self.fitness
171    }
172
173    fn random<R: Rng>(rng: &mut R) -> Self {
174        // Default: empty, will be overridden by problem's initialize_population
175        Self::random_with_options(0, 1, rng)
176    }
177
178    fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
179        self.order_crossover(other, rng)
180    }
181
182    fn mutate<R: Rng>(&mut self, rng: &mut R) {
183        // 50% swap, 30% inversion, 20% rotation
184        let r: f64 = rng.gen();
185        if r < 0.5 {
186            self.swap_mutate(rng);
187        } else if r < 0.8 {
188            self.inversion_mutate(rng);
189        } else {
190            // Rotation mutation with 4 options (0, 90, 180, 270 degrees)
191            self.rotation_mutate(4, rng);
192        }
193    }
194}
195
196/// Instance information for decoding.
197#[derive(Debug, Clone)]
198struct InstanceInfo {
199    /// Index into the geometries array.
200    geometry_idx: usize,
201    /// Instance number within this geometry's quantity.
202    instance_num: usize,
203}
204
205/// Problem definition for GA-based 2D nesting.
206pub struct NestingProblem {
207    /// Input geometries.
208    geometries: Vec<Geometry2D>,
209    /// Boundary container.
210    boundary: Boundary2D,
211    /// Solver configuration.
212    config: Config,
213    /// Instance mapping (instance_id -> (geometry_idx, instance_num)).
214    instances: Vec<InstanceInfo>,
215    /// Available rotation angles per geometry.
216    rotation_angles: Vec<Vec<f64>>,
217    /// Number of rotation options.
218    rotation_options: usize,
219    /// Cancellation flag.
220    cancelled: Arc<AtomicBool>,
221}
222
223impl NestingProblem {
224    /// Creates a new nesting problem.
225    pub fn new(
226        geometries: Vec<Geometry2D>,
227        boundary: Boundary2D,
228        config: Config,
229        cancelled: Arc<AtomicBool>,
230    ) -> Self {
231        // Build instance mapping
232        let mut instances = Vec::new();
233        let mut rotation_angles = Vec::new();
234
235        for (geom_idx, geom) in geometries.iter().enumerate() {
236            // Get rotation angles for this geometry
237            let angles = geom.rotations();
238            let angles = if angles.is_empty() { vec![0.0] } else { angles };
239            rotation_angles.push(angles);
240
241            // Create instances
242            for instance_num in 0..geom.quantity() {
243                instances.push(InstanceInfo {
244                    geometry_idx: geom_idx,
245                    instance_num,
246                });
247            }
248        }
249
250        // Maximum rotation options across all geometries
251        let rotation_options = rotation_angles.iter().map(|a| a.len()).max().unwrap_or(1);
252
253        Self {
254            geometries,
255            boundary,
256            config,
257            instances,
258            rotation_angles,
259            rotation_options,
260            cancelled,
261        }
262    }
263
264    /// Returns the total number of instances.
265    pub fn num_instances(&self) -> usize {
266        self.instances.len()
267    }
268
269    /// Returns the number of rotation options.
270    pub fn rotation_options(&self) -> usize {
271        self.rotation_options
272    }
273
274    /// Decodes a chromosome into placements using NFP-guided placement.
275    pub fn decode(&self, chromosome: &NestingChromosome) -> (Vec<Placement<f64>>, f64, usize) {
276        let mut placements = Vec::new();
277        let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
278        let mut total_placed_area = 0.0;
279        let mut placed_count = 0;
280
281        let margin = self.config.margin;
282        let spacing = self.config.spacing;
283
284        // Get boundary polygon with margin
285        let boundary_polygon = self.get_boundary_polygon_with_margin(margin);
286
287        // Sampling step for grid search
288        let sample_step = self.compute_sample_step();
289
290        // Place geometries in the order specified by chromosome
291        for &instance_idx in chromosome.order.iter() {
292            if self.cancelled.load(Ordering::Relaxed) {
293                break;
294            }
295
296            if instance_idx >= self.instances.len() {
297                continue;
298            }
299
300            let info = &self.instances[instance_idx];
301            let geom = &self.geometries[info.geometry_idx];
302
303            // Get rotation angle from chromosome
304            let rotation_idx = chromosome.rotations.get(instance_idx).copied().unwrap_or(0);
305            let rotation_angle = self
306                .rotation_angles
307                .get(info.geometry_idx)
308                .and_then(|angles| angles.get(rotation_idx % angles.len()))
309                .copied()
310                .unwrap_or(0.0);
311
312            // Compute IFP for this geometry at this rotation
313            let ifp = match compute_ifp(&boundary_polygon, geom, rotation_angle) {
314                Ok(ifp) => ifp,
315                Err(_) => {
316                    continue;
317                }
318            };
319
320            if ifp.is_empty() {
321                continue;
322            }
323
324            // Compute NFPs with all placed geometries
325            let mut nfps: Vec<Nfp> = Vec::new();
326            for placed in &placed_geometries {
327                let placed_exterior = placed.translated_exterior();
328                let placed_geom = Geometry2D::new(format!("_placed_{}", placed.geometry.id()))
329                    .with_polygon(placed_exterior);
330
331                if let Ok(nfp) = compute_nfp(&placed_geom, geom, rotation_angle) {
332                    let expanded = self.expand_nfp(&nfp, spacing);
333                    nfps.push(expanded);
334                }
335            }
336
337            // Shrink IFP by spacing
338            let ifp_shrunk = self.shrink_ifp(&ifp, spacing);
339
340            // Find the bottom-left valid placement
341            // IFP returns positions where the geometry's origin should be placed.
342            // Clamp to ensure placement keeps geometry within boundary.
343            let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
344            let placement_result = find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step);
345            if let Some((x, y)) = placement_result {
346                // Clamp position to keep geometry within boundary
347                let geom_aabb = geom.aabb_at_rotation(rotation_angle);
348                let boundary_aabb = self.boundary.aabb();
349
350                if let Some((clamped_x, clamped_y)) =
351                    clamp_placement_to_boundary(x, y, geom_aabb, boundary_aabb)
352                {
353                    let placement = Placement::new_2d(
354                        geom.id().clone(),
355                        info.instance_num,
356                        clamped_x,
357                        clamped_y,
358                        rotation_angle,
359                    );
360
361                    placements.push(placement);
362                    placed_geometries.push(PlacedGeometry::new(
363                        geom.clone(),
364                        (clamped_x, clamped_y),
365                        rotation_angle,
366                    ));
367                    total_placed_area += geom.measure();
368                    placed_count += 1;
369                }
370            }
371        }
372
373        let utilization = total_placed_area / self.boundary.measure();
374        (placements, utilization, placed_count)
375    }
376
377    /// Gets the boundary polygon with margin applied.
378    fn get_boundary_polygon_with_margin(&self, margin: f64) -> Vec<(f64, f64)> {
379        let (b_min, b_max) = self.boundary.aabb();
380        vec![
381            (b_min[0] + margin, b_min[1] + margin),
382            (b_max[0] - margin, b_min[1] + margin),
383            (b_max[0] - margin, b_max[1] - margin),
384            (b_min[0] + margin, b_max[1] - margin),
385        ]
386    }
387
388    /// Computes an adaptive sample step based on geometry sizes.
389    fn compute_sample_step(&self) -> f64 {
390        if self.geometries.is_empty() {
391            return 1.0;
392        }
393
394        let mut min_dim = f64::INFINITY;
395        for geom in &self.geometries {
396            let (g_min, g_max) = geom.aabb();
397            let width = g_max[0] - g_min[0];
398            let height = g_max[1] - g_min[1];
399            min_dim = min_dim.min(width).min(height);
400        }
401
402        (min_dim / 4.0).clamp(0.5, 10.0)
403    }
404
405    /// Expands an NFP by the given spacing amount.
406    fn expand_nfp(&self, nfp: &Nfp, spacing: f64) -> Nfp {
407        if spacing <= 0.0 {
408            return nfp.clone();
409        }
410
411        let expanded_polygons: Vec<Vec<(f64, f64)>> = nfp
412            .polygons
413            .iter()
414            .map(|polygon| {
415                let (cx, cy) = polygon_centroid(polygon);
416                polygon
417                    .iter()
418                    .map(|&(x, y)| {
419                        let dx = x - cx;
420                        let dy = y - cy;
421                        let dist = (dx * dx + dy * dy).sqrt();
422                        if dist > 1e-10 {
423                            let scale = (dist + spacing) / dist;
424                            (cx + dx * scale, cy + dy * scale)
425                        } else {
426                            (x, y)
427                        }
428                    })
429                    .collect()
430            })
431            .collect();
432
433        Nfp::from_polygons(expanded_polygons)
434    }
435
436    /// Shrinks an IFP by the given spacing amount.
437    fn shrink_ifp(&self, ifp: &Nfp, spacing: f64) -> Nfp {
438        if spacing <= 0.0 {
439            return ifp.clone();
440        }
441
442        let shrunk_polygons: Vec<Vec<(f64, f64)>> = ifp
443            .polygons
444            .iter()
445            .filter_map(|polygon| {
446                let (cx, cy) = polygon_centroid(polygon);
447                let shrunk: Vec<(f64, f64)> = polygon
448                    .iter()
449                    .map(|&(x, y)| {
450                        let dx = x - cx;
451                        let dy = y - cy;
452                        let dist = (dx * dx + dy * dy).sqrt();
453                        if dist > spacing + 1e-10 {
454                            let scale = (dist - spacing) / dist;
455                            (cx + dx * scale, cy + dy * scale)
456                        } else {
457                            (cx, cy)
458                        }
459                    })
460                    .collect();
461
462                if shrunk.len() >= 3 {
463                    Some(shrunk)
464                } else {
465                    None
466                }
467            })
468            .collect();
469
470        Nfp::from_polygons(shrunk_polygons)
471    }
472}
473
474/// Computes the centroid of a polygon.
475fn polygon_centroid(polygon: &[(f64, f64)]) -> (f64, f64) {
476    if polygon.is_empty() {
477        return (0.0, 0.0);
478    }
479
480    let sum: (f64, f64) = polygon
481        .iter()
482        .fold((0.0, 0.0), |acc, &(x, y)| (acc.0 + x, acc.1 + y));
483    let n = polygon.len() as f64;
484    (sum.0 / n, sum.1 / n)
485}
486
487impl GaProblem for NestingProblem {
488    type Individual = NestingChromosome;
489
490    fn evaluate(&self, individual: &mut Self::Individual) {
491        let (_, utilization, placed_count) = self.decode(individual);
492
493        // Fitness = utilization + bonus for placing all pieces
494        let placement_ratio = placed_count as f64 / individual.total_count.max(1) as f64;
495
496        // Primary: maximize placement ratio (most important)
497        // Secondary: maximize utilization
498        let fitness = placement_ratio * 100.0 + utilization * 10.0;
499
500        individual.set_fitness(fitness, placed_count);
501    }
502
503    fn initialize_population<R: Rng>(&self, size: usize, rng: &mut R) -> Vec<Self::Individual> {
504        (0..size)
505            .map(|_| {
506                NestingChromosome::random_with_options(
507                    self.num_instances(),
508                    self.rotation_options(),
509                    rng,
510                )
511            })
512            .collect()
513    }
514
515    fn on_generation(
516        &self,
517        generation: u32,
518        best: &Self::Individual,
519        _population: &[Self::Individual],
520    ) {
521        log::debug!(
522            "GA Generation {}: fitness={:.4}, placed={}/{}",
523            generation,
524            best.fitness(),
525            best.placed_count,
526            best.total_count
527        );
528    }
529}
530
531/// Runs GA-based nesting optimization.
532pub fn run_ga_nesting(
533    geometries: &[Geometry2D],
534    boundary: &Boundary2D,
535    config: &Config,
536    ga_config: GaConfig,
537    cancelled: Arc<AtomicBool>,
538) -> SolveResult<f64> {
539    let problem = NestingProblem::new(
540        geometries.to_vec(),
541        boundary.clone(),
542        config.clone(),
543        cancelled.clone(),
544    );
545
546    let runner = GaRunner::new(ga_config, problem);
547
548    // Connect cancellation
549    let cancel_handle = runner.cancel_handle();
550    let cancelled_clone = cancelled.clone();
551    std::thread::spawn(move || {
552        while !cancelled_clone.load(Ordering::Relaxed) {
553            std::thread::sleep(std::time::Duration::from_millis(100));
554        }
555        cancel_handle.store(true, Ordering::Relaxed);
556    });
557
558    let ga_result = runner.run();
559
560    // Decode the best chromosome to get final placements
561    let problem = NestingProblem::new(
562        geometries.to_vec(),
563        boundary.clone(),
564        config.clone(),
565        Arc::new(AtomicBool::new(false)),
566    );
567
568    let (placements, utilization, _placed_count) = problem.decode(&ga_result.best);
569
570    // Build unplaced list
571    let mut unplaced = Vec::new();
572    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
573    for p in &placements {
574        placed_ids.insert(p.geometry_id.clone());
575    }
576    for geom in geometries {
577        if !placed_ids.contains(geom.id()) {
578            unplaced.push(geom.id().clone());
579        }
580    }
581
582    let mut result = SolveResult::new();
583    result.placements = placements;
584    result.unplaced = unplaced;
585    result.boundaries_used = 1;
586    result.utilization = utilization;
587    result.computation_time_ms = ga_result.elapsed.as_millis() as u64;
588    result.generations = Some(ga_result.generations);
589    result.best_fitness = Some(ga_result.best.fitness());
590    result.fitness_history = Some(ga_result.history);
591    result.strategy = Some("GeneticAlgorithm".to_string());
592    result.cancelled = cancelled.load(Ordering::Relaxed);
593    result.target_reached = ga_result.target_reached;
594
595    result
596}
597
598/// Runs GA-based nesting optimization with progress callback.
599pub fn run_ga_nesting_with_progress(
600    geometries: &[Geometry2D],
601    boundary: &Boundary2D,
602    config: &Config,
603    ga_config: GaConfig,
604    cancelled: Arc<AtomicBool>,
605    progress_callback: ProgressCallback,
606) -> SolveResult<f64> {
607    let total_items = geometries.iter().map(|g| g.quantity()).sum::<usize>();
608
609    let problem = NestingProblem::new(
610        geometries.to_vec(),
611        boundary.clone(),
612        config.clone(),
613        cancelled.clone(),
614    );
615
616    let runner = GaRunner::new(ga_config.clone(), problem);
617
618    // Connect cancellation
619    let cancel_handle = runner.cancel_handle();
620    let cancelled_clone = cancelled.clone();
621    std::thread::spawn(move || {
622        while !cancelled_clone.load(Ordering::Relaxed) {
623            std::thread::sleep(std::time::Duration::from_millis(100));
624        }
625        cancel_handle.store(true, Ordering::Relaxed);
626    });
627
628    // Run GA with progress callback adapter
629    let max_generations = ga_config.max_generations;
630    let ga_result = runner.run_with_progress(move |ga_progress: GaProgress<f64>| {
631        let info = ProgressInfo::new()
632            .with_iteration(ga_progress.generation, max_generations)
633            .with_fitness(ga_progress.best_fitness)
634            .with_utilization(ga_progress.best_fitness) // fitness is utilization
635            .with_items(0, total_items) // we don't track placed count during GA
636            .with_elapsed(ga_progress.elapsed.as_millis() as u64)
637            .with_phase("Genetic Algorithm".to_string());
638
639        let info = if !ga_progress.running {
640            info.finished()
641        } else {
642            info
643        };
644
645        progress_callback(info);
646    });
647
648    // Decode the best chromosome to get final placements
649    let problem = NestingProblem::new(
650        geometries.to_vec(),
651        boundary.clone(),
652        config.clone(),
653        Arc::new(AtomicBool::new(false)),
654    );
655
656    let (placements, utilization, _placed_count) = problem.decode(&ga_result.best);
657
658    // Build unplaced list
659    let mut unplaced = Vec::new();
660    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
661    for p in &placements {
662        placed_ids.insert(p.geometry_id.clone());
663    }
664    for geom in geometries {
665        if !placed_ids.contains(geom.id()) {
666            unplaced.push(geom.id().clone());
667        }
668    }
669
670    let mut result = SolveResult::new();
671    result.placements = placements;
672    result.unplaced = unplaced;
673    result.boundaries_used = 1;
674    result.utilization = utilization;
675    result.computation_time_ms = ga_result.elapsed.as_millis() as u64;
676    result.generations = Some(ga_result.generations);
677    result.best_fitness = Some(ga_result.best.fitness());
678    result.fitness_history = Some(ga_result.history);
679    result.strategy = Some("GeneticAlgorithm".to_string());
680    result.cancelled = cancelled.load(Ordering::Relaxed);
681    result.target_reached = ga_result.target_reached;
682
683    result
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689
690    #[test]
691    fn test_nesting_chromosome_crossover() {
692        let mut rng = thread_rng();
693        let parent1 = NestingChromosome::random_with_options(10, 4, &mut rng);
694        let parent2 = NestingChromosome::random_with_options(10, 4, &mut rng);
695
696        let child = parent1.order_crossover(&parent2, &mut rng);
697
698        // Child should be a valid permutation
699        assert_eq!(child.order.len(), 10);
700        let mut sorted = child.order.clone();
701        sorted.sort();
702        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
703    }
704
705    #[test]
706    fn test_nesting_chromosome_mutation() {
707        let mut rng = thread_rng();
708        let mut chromosome = NestingChromosome::random_with_options(10, 4, &mut rng);
709
710        chromosome.swap_mutate(&mut rng);
711
712        // Should still be a valid permutation
713        let mut sorted = chromosome.order.clone();
714        sorted.sort();
715        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
716    }
717
718    #[test]
719    fn test_ga_nesting_basic() {
720        let geometries = vec![
721            Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
722            Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
723        ];
724
725        let boundary = Boundary2D::rectangle(100.0, 50.0);
726        let config = Config::default();
727        let ga_config = GaConfig::default()
728            .with_population_size(20)
729            .with_max_generations(10);
730
731        let result = run_ga_nesting(
732            &geometries,
733            &boundary,
734            &config,
735            ga_config,
736            Arc::new(AtomicBool::new(false)),
737        );
738
739        assert!(result.utilization > 0.0);
740        assert!(!result.placements.is_empty());
741    }
742
743    #[test]
744    fn test_ga_nesting_all_placed() {
745        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
746
747        let boundary = Boundary2D::rectangle(100.0, 100.0);
748        let config = Config::default();
749        let ga_config = GaConfig::default()
750            .with_population_size(30)
751            .with_max_generations(20);
752
753        let result = run_ga_nesting(
754            &geometries,
755            &boundary,
756            &config,
757            ga_config,
758            Arc::new(AtomicBool::new(false)),
759        );
760
761        // All 4 pieces should fit easily
762        assert_eq!(result.placements.len(), 4);
763        assert!(result.unplaced.is_empty());
764    }
765
766    #[test]
767    fn test_ga_nesting_with_rotation() {
768        // L-shaped pieces that might benefit from rotation
769        let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
770            .with_quantity(3)
771            .with_rotations(vec![0.0, 90.0])];
772
773        let boundary = Boundary2D::rectangle(50.0, 50.0);
774        let config = Config::default();
775        let ga_config = GaConfig::default()
776            .with_population_size(30)
777            .with_max_generations(20);
778
779        let result = run_ga_nesting(
780            &geometries,
781            &boundary,
782            &config,
783            ga_config,
784            Arc::new(AtomicBool::new(false)),
785        );
786
787        assert!(result.utilization > 0.0);
788        // Should be able to place at least some pieces
789        assert!(!result.placements.is_empty());
790    }
791
792    #[test]
793    fn test_nesting_problem_decode() {
794        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2)];
795
796        let boundary = Boundary2D::rectangle(100.0, 50.0);
797        let config = Config::default();
798        let cancelled = Arc::new(AtomicBool::new(false));
799
800        let problem = NestingProblem::new(geometries, boundary, config, cancelled);
801
802        assert_eq!(problem.num_instances(), 2);
803
804        // Create a chromosome and decode
805        let chromosome = NestingChromosome::new(2, 1);
806        let (placements, utilization, placed_count) = problem.decode(&chromosome);
807
808        assert_eq!(placed_count, 2);
809        assert_eq!(placements.len(), 2);
810        assert!(utilization > 0.0);
811    }
812}