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    let ga_result = runner.run();
493
494    // Decode the best chromosome to get final placements
495    let problem = NestingProblem::new(
496        geometries.to_vec(),
497        boundary.clone(),
498        config.clone(),
499        Arc::new(AtomicBool::new(false)),
500    );
501
502    let (placements, utilization, _placed_count) = problem.decode(&ga_result.best);
503
504    // Build unplaced list
505    let mut unplaced = Vec::new();
506    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
507    for p in &placements {
508        placed_ids.insert(p.geometry_id.clone());
509    }
510    for geom in geometries {
511        if !placed_ids.contains(geom.id()) {
512            unplaced.push(geom.id().clone());
513        }
514    }
515
516    let mut result = SolveResult::new();
517    result.placements = placements;
518    result.unplaced = unplaced;
519    result.boundaries_used = 1;
520    result.utilization = utilization;
521    result.computation_time_ms = ga_result.elapsed.as_millis() as u64;
522    result.generations = Some(ga_result.generations);
523    result.best_fitness = Some(ga_result.best.fitness());
524    result.fitness_history = Some(ga_result.history);
525    result.strategy = Some("GeneticAlgorithm".to_string());
526    result.cancelled = cancelled.load(Ordering::Relaxed);
527    result.target_reached = ga_result.target_reached;
528
529    result
530}
531
532/// Runs GA-based nesting optimization with progress callback.
533pub fn run_ga_nesting_with_progress(
534    geometries: &[Geometry2D],
535    boundary: &Boundary2D,
536    config: &Config,
537    ga_config: GaConfig,
538    cancelled: Arc<AtomicBool>,
539    progress_callback: ProgressCallback,
540) -> SolveResult<f64> {
541    let total_items = geometries.iter().map(|g| g.quantity()).sum::<usize>();
542
543    let problem = NestingProblem::new(
544        geometries.to_vec(),
545        boundary.clone(),
546        config.clone(),
547        cancelled.clone(),
548    );
549
550    let runner = GaRunner::new(ga_config.clone(), problem);
551
552    // Connect cancellation (thread-based polling, not available on WASM)
553    #[cfg(not(target_arch = "wasm32"))]
554    {
555        let cancel_handle = runner.cancel_handle();
556        let cancelled_clone = cancelled.clone();
557        std::thread::spawn(move || {
558            while !cancelled_clone.load(Ordering::Relaxed) {
559                std::thread::sleep(std::time::Duration::from_millis(100));
560            }
561            cancel_handle.store(true, Ordering::Relaxed);
562        });
563    }
564
565    // Run GA with progress callback adapter
566    let max_generations = ga_config.max_generations;
567    let ga_result = runner.run_with_progress(move |ga_progress: GaProgress<f64>| {
568        let info = ProgressInfo::new()
569            .with_iteration(ga_progress.generation, max_generations)
570            .with_fitness(ga_progress.best_fitness)
571            .with_utilization(ga_progress.best_fitness) // fitness is utilization
572            .with_items(0, total_items) // we don't track placed count during GA
573            .with_elapsed(ga_progress.elapsed.as_millis() as u64)
574            .with_phase("Genetic Algorithm".to_string());
575
576        let info = if !ga_progress.running {
577            info.finished()
578        } else {
579            info
580        };
581
582        progress_callback(info);
583    });
584
585    // Decode the best chromosome to get final placements
586    let problem = NestingProblem::new(
587        geometries.to_vec(),
588        boundary.clone(),
589        config.clone(),
590        Arc::new(AtomicBool::new(false)),
591    );
592
593    let (placements, utilization, _placed_count) = problem.decode(&ga_result.best);
594
595    // Build unplaced list
596    let mut unplaced = Vec::new();
597    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
598    for p in &placements {
599        placed_ids.insert(p.geometry_id.clone());
600    }
601    for geom in geometries {
602        if !placed_ids.contains(geom.id()) {
603            unplaced.push(geom.id().clone());
604        }
605    }
606
607    let mut result = SolveResult::new();
608    result.placements = placements;
609    result.unplaced = unplaced;
610    result.boundaries_used = 1;
611    result.utilization = utilization;
612    result.computation_time_ms = ga_result.elapsed.as_millis() as u64;
613    result.generations = Some(ga_result.generations);
614    result.best_fitness = Some(ga_result.best.fitness());
615    result.fitness_history = Some(ga_result.history);
616    result.strategy = Some("GeneticAlgorithm".to_string());
617    result.cancelled = cancelled.load(Ordering::Relaxed);
618    result.target_reached = ga_result.target_reached;
619
620    result
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626
627    #[test]
628    fn test_nesting_chromosome_crossover() {
629        let mut rng = rand::rng();
630        let parent1 = NestingChromosome::random_with_options(10, 4, &mut rng);
631        let parent2 = NestingChromosome::random_with_options(10, 4, &mut rng);
632
633        let child = parent1.order_crossover(&parent2, &mut rng);
634
635        // Child should be a valid permutation
636        assert_eq!(child.order.len(), 10);
637        let mut sorted = child.order.clone();
638        sorted.sort();
639        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
640    }
641
642    #[test]
643    fn test_nesting_chromosome_mutation() {
644        let mut rng = rand::rng();
645        let mut chromosome = NestingChromosome::random_with_options(10, 4, &mut rng);
646
647        chromosome.swap_mutate(&mut rng);
648
649        // Should still be a valid permutation
650        let mut sorted = chromosome.order.clone();
651        sorted.sort();
652        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
653    }
654
655    #[test]
656    fn test_ga_nesting_basic() {
657        let geometries = vec![
658            Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
659            Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
660        ];
661
662        let boundary = Boundary2D::rectangle(100.0, 50.0);
663        let config = Config::default();
664        let ga_config = GaConfig::default()
665            .with_population_size(20)
666            .with_max_generations(10);
667
668        let result = run_ga_nesting(
669            &geometries,
670            &boundary,
671            &config,
672            ga_config,
673            Arc::new(AtomicBool::new(false)),
674        );
675
676        assert!(result.utilization > 0.0);
677        assert!(!result.placements.is_empty());
678    }
679
680    #[test]
681    fn test_ga_nesting_all_placed() {
682        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
683
684        let boundary = Boundary2D::rectangle(100.0, 100.0);
685        let config = Config::default();
686        let ga_config = GaConfig::default()
687            .with_population_size(30)
688            .with_max_generations(20);
689
690        let result = run_ga_nesting(
691            &geometries,
692            &boundary,
693            &config,
694            ga_config,
695            Arc::new(AtomicBool::new(false)),
696        );
697
698        // All 4 pieces should fit easily
699        assert_eq!(result.placements.len(), 4);
700        assert!(result.unplaced.is_empty());
701    }
702
703    #[test]
704    fn test_ga_nesting_with_rotation() {
705        // L-shaped pieces that might benefit from rotation
706        let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
707            .with_quantity(3)
708            .with_rotations(vec![0.0, 90.0])];
709
710        let boundary = Boundary2D::rectangle(50.0, 50.0);
711        let config = Config::default();
712        let ga_config = GaConfig::default()
713            .with_population_size(30)
714            .with_max_generations(20);
715
716        let result = run_ga_nesting(
717            &geometries,
718            &boundary,
719            &config,
720            ga_config,
721            Arc::new(AtomicBool::new(false)),
722        );
723
724        assert!(result.utilization > 0.0);
725        // Should be able to place at least some pieces
726        assert!(!result.placements.is_empty());
727    }
728
729    #[test]
730    fn test_nesting_problem_decode() {
731        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2)];
732
733        let boundary = Boundary2D::rectangle(100.0, 50.0);
734        let config = Config::default();
735        let cancelled = Arc::new(AtomicBool::new(false));
736
737        let problem = NestingProblem::new(geometries, boundary, config, cancelled);
738
739        assert_eq!(problem.num_instances(), 2);
740
741        // Create a chromosome and decode
742        let chromosome = NestingChromosome::new(2, 1);
743        let (placements, utilization, placed_count) = problem.decode(&chromosome);
744
745        assert_eq!(placed_count, 2);
746        assert_eq!(placements.len(), 2);
747        assert!(utilization > 0.0);
748    }
749}