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
571    let max_generations = ga_config.max_generations;
572    let ga_result = runner.run_with_progress(move |ga_progress: GaProgress<f64>| {
573        let info = ProgressInfo::new()
574            .with_iteration(ga_progress.generation, max_generations)
575            .with_fitness(ga_progress.best_fitness)
576            .with_utilization(ga_progress.best_fitness) // fitness is utilization
577            .with_items(0, total_items) // we don't track placed count during GA
578            .with_elapsed(ga_progress.elapsed.as_millis() as u64)
579            .with_phase("Genetic Algorithm".to_string());
580
581        let info = if !ga_progress.running {
582            info.finished()
583        } else {
584            info
585        };
586
587        progress_callback(info);
588    });
589
590    // Decode the best chromosome to get final placements
591    let problem = NestingProblem::new(
592        geometries.to_vec(),
593        boundary.clone(),
594        config.clone(),
595        Arc::new(AtomicBool::new(false)),
596    );
597
598    let (placements, utilization, _placed_count) = problem.decode(&ga_result.best);
599
600    // Build unplaced list
601    let mut unplaced = Vec::new();
602    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
603    for p in &placements {
604        placed_ids.insert(p.geometry_id.clone());
605    }
606    for geom in geometries {
607        if !placed_ids.contains(geom.id()) {
608            unplaced.push(geom.id().clone());
609        }
610    }
611
612    let mut result = SolveResult::new();
613    result.placements = placements;
614    result.unplaced = unplaced;
615    result.boundaries_used = 1;
616    result.utilization = utilization;
617    result.computation_time_ms = ga_result.elapsed.as_millis() as u64;
618    result.generations = Some(ga_result.generations);
619    result.best_fitness = Some(ga_result.best.fitness());
620    result.fitness_history = Some(ga_result.history);
621    result.strategy = Some("GeneticAlgorithm".to_string());
622    result.cancelled = cancelled.load(Ordering::Relaxed);
623    result.target_reached = ga_result.target_reached;
624
625    result
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631
632    #[test]
633    fn test_nesting_chromosome_crossover() {
634        let mut rng = rand::rng();
635        let parent1 = NestingChromosome::random_with_options(10, 4, &mut rng);
636        let parent2 = NestingChromosome::random_with_options(10, 4, &mut rng);
637
638        let child = parent1.order_crossover(&parent2, &mut rng);
639
640        // Child should be a valid permutation
641        assert_eq!(child.order.len(), 10);
642        let mut sorted = child.order.clone();
643        sorted.sort();
644        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
645    }
646
647    #[test]
648    fn test_nesting_chromosome_mutation() {
649        let mut rng = rand::rng();
650        let mut chromosome = NestingChromosome::random_with_options(10, 4, &mut rng);
651
652        chromosome.swap_mutate(&mut rng);
653
654        // Should still be a valid permutation
655        let mut sorted = chromosome.order.clone();
656        sorted.sort();
657        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
658    }
659
660    #[test]
661    fn test_ga_nesting_basic() {
662        let geometries = vec![
663            Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
664            Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
665        ];
666
667        let boundary = Boundary2D::rectangle(100.0, 50.0);
668        let config = Config::default();
669        let ga_config = GaConfig::default()
670            .with_population_size(20)
671            .with_max_generations(10);
672
673        let result = run_ga_nesting(
674            &geometries,
675            &boundary,
676            &config,
677            ga_config,
678            Arc::new(AtomicBool::new(false)),
679        );
680
681        assert!(result.utilization > 0.0);
682        assert!(!result.placements.is_empty());
683    }
684
685    #[test]
686    fn test_ga_nesting_all_placed() {
687        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
688
689        let boundary = Boundary2D::rectangle(100.0, 100.0);
690        let config = Config::default();
691        let ga_config = GaConfig::default()
692            .with_population_size(30)
693            .with_max_generations(20);
694
695        let result = run_ga_nesting(
696            &geometries,
697            &boundary,
698            &config,
699            ga_config,
700            Arc::new(AtomicBool::new(false)),
701        );
702
703        // All 4 pieces should fit easily
704        assert_eq!(result.placements.len(), 4);
705        assert!(result.unplaced.is_empty());
706    }
707
708    #[test]
709    fn test_ga_nesting_with_rotation() {
710        // L-shaped pieces that might benefit from rotation
711        let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
712            .with_quantity(3)
713            .with_rotations(vec![0.0, 90.0])];
714
715        let boundary = Boundary2D::rectangle(50.0, 50.0);
716        let config = Config::default();
717        let ga_config = GaConfig::default()
718            .with_population_size(30)
719            .with_max_generations(20);
720
721        let result = run_ga_nesting(
722            &geometries,
723            &boundary,
724            &config,
725            ga_config,
726            Arc::new(AtomicBool::new(false)),
727        );
728
729        assert!(result.utilization > 0.0);
730        // Should be able to place at least some pieces
731        assert!(!result.placements.is_empty());
732    }
733
734    #[test]
735    fn test_nesting_problem_decode() {
736        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2)];
737
738        let boundary = Boundary2D::rectangle(100.0, 50.0);
739        let config = Config::default();
740        let cancelled = Arc::new(AtomicBool::new(false));
741
742        let problem = NestingProblem::new(geometries, boundary, config, cancelled);
743
744        assert_eq!(problem.num_instances(), 2);
745
746        // Create a chromosome and decode
747        let chromosome = NestingChromosome::new(2, 1);
748        let (placements, utilization, placed_count) = problem.decode(&chromosome);
749
750        assert_eq!(placed_count, 2);
751        assert_eq!(placements.len(), 2);
752        assert!(utilization > 0.0);
753    }
754}