Skip to main content

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