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_with_margin_and_mirror, compute_nfp_mirrored, find_bottom_left_placement,
11    verify_no_overlap_mirrored, Nfp, PlacedGeometry,
12};
13use rand::prelude::*;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::Arc;
16use u_nesting_core::ga::{GaConfig, GaProblem, GaProgress, GaRunner, Individual};
17use u_nesting_core::geometry::{Boundary, Geometry};
18use u_nesting_core::solver::{Config, ProgressCallback, ProgressInfo};
19use u_nesting_core::{Placement, SolveResult};
20
21use crate::placement_utils::{expand_nfp, nesting_fitness, shrink_ifp, InstanceInfo};
22
23/// Nesting chromosome representing a placement order and rotations.
24#[derive(Debug, Clone)]
25pub struct NestingChromosome {
26    /// Permutation of geometry indices (placement order).
27    pub order: Vec<usize>,
28    /// Rotation index for each geometry instance.
29    pub rotations: Vec<usize>,
30    /// Mirror flag for each geometry instance (`allow_flip` support).
31    ///
32    /// Populated unconditionally for every instance, same as `rotations` —
33    /// `decode()` masks it against the instance's own `Geometry2D::allow_flip()`,
34    /// the same modulo-based tolerance `rotations` already uses for
35    /// geometries with fewer rotation options than the population-wide max.
36    /// A gene for a non-flippable geometry is simply never read.
37    pub mirrors: Vec<bool>,
38    /// Cached fitness value.
39    fitness: f64,
40    /// Number of placed pieces (for fitness calculation).
41    placed_count: usize,
42    /// Total instances count.
43    total_count: usize,
44}
45
46impl NestingChromosome {
47    /// Creates a new chromosome for the given number of instances and rotation options.
48    pub fn new(num_instances: usize, _rotation_options: usize) -> Self {
49        Self {
50            order: (0..num_instances).collect(),
51            rotations: vec![0; num_instances],
52            mirrors: vec![false; num_instances],
53            fitness: f64::NEG_INFINITY,
54            placed_count: 0,
55            total_count: num_instances,
56        }
57    }
58
59    /// Creates a random chromosome.
60    pub fn random_with_options<R: Rng>(
61        num_instances: usize,
62        rotation_options: usize,
63        rng: &mut R,
64    ) -> Self {
65        let mut order: Vec<usize> = (0..num_instances).collect();
66        order.shuffle(rng);
67
68        let rotations: Vec<usize> = (0..num_instances)
69            .map(|_| rng.random_range(0..rotation_options.max(1)))
70            .collect();
71
72        let mirrors: Vec<bool> = (0..num_instances).map(|_| rng.random()).collect();
73
74        Self {
75            order,
76            rotations,
77            mirrors,
78            fitness: f64::NEG_INFINITY,
79            placed_count: 0,
80            total_count: num_instances,
81        }
82    }
83
84    /// Sets the fitness value.
85    pub fn set_fitness(&mut self, fitness: f64, placed_count: usize) {
86        self.fitness = fitness;
87        self.placed_count = placed_count;
88    }
89
90    /// Order crossover (OX1) for permutation genes.
91    pub fn order_crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
92        let n = self.order.len();
93        if n < 2 {
94            return self.clone();
95        }
96
97        // Select two crossover points
98        let (mut p1, mut p2) = (rng.random_range(0..n), rng.random_range(0..n));
99        if p1 > p2 {
100            std::mem::swap(&mut p1, &mut p2);
101        }
102
103        // Copy segment from parent1
104        let mut child_order = vec![usize::MAX; n];
105        let mut used = vec![false; n];
106
107        for i in p1..=p2 {
108            child_order[i] = self.order[i];
109            used[self.order[i]] = true;
110        }
111
112        // Fill remaining from parent2
113        let mut j = (p2 + 1) % n;
114        for i in 0..n {
115            let idx = (p2 + 1 + i) % n;
116            if child_order[idx] == usize::MAX {
117                while used[other.order[j]] {
118                    j = (j + 1) % n;
119                }
120                child_order[idx] = other.order[j];
121                used[other.order[j]] = true;
122                j = (j + 1) % n;
123            }
124        }
125
126        // Crossover rotations (uniform)
127        let rotations: Vec<usize> = self
128            .rotations
129            .iter()
130            .zip(&other.rotations)
131            .map(|(a, b)| if rng.random() { *a } else { *b })
132            .collect();
133
134        // Crossover mirrors (uniform), same pattern as rotations.
135        let mirrors: Vec<bool> = self
136            .mirrors
137            .iter()
138            .zip(&other.mirrors)
139            .map(|(a, b)| if rng.random() { *a } else { *b })
140            .collect();
141
142        Self {
143            order: child_order,
144            rotations,
145            mirrors,
146            fitness: f64::NEG_INFINITY,
147            placed_count: 0,
148            total_count: self.total_count,
149        }
150    }
151
152    /// Swap mutation for order genes.
153    pub fn swap_mutate<R: Rng>(&mut self, rng: &mut R) {
154        if self.order.len() < 2 {
155            return;
156        }
157
158        let i = rng.random_range(0..self.order.len());
159        let j = rng.random_range(0..self.order.len());
160        self.order.swap(i, j);
161        self.fitness = f64::NEG_INFINITY;
162    }
163
164    /// Rotation mutation.
165    pub fn rotation_mutate<R: Rng>(&mut self, rotation_options: usize, rng: &mut R) {
166        if self.rotations.is_empty() || rotation_options <= 1 {
167            return;
168        }
169
170        let idx = rng.random_range(0..self.rotations.len());
171        self.rotations[idx] = rng.random_range(0..rotation_options);
172        self.fitness = f64::NEG_INFINITY;
173    }
174
175    /// Mirror-flag mutation (`allow_flip` support): flips one instance's
176    /// mirror bit. Unconditional on the gene, same as `rotation_mutate` —
177    /// `decode()` is what masks a flip against the instance's own
178    /// `allow_flip()`, so mutating a non-flippable instance's bit is harmless.
179    pub fn mirror_mutate<R: Rng>(&mut self, rng: &mut R) {
180        if self.mirrors.is_empty() {
181            return;
182        }
183
184        let idx = rng.random_range(0..self.mirrors.len());
185        self.mirrors[idx] = !self.mirrors[idx];
186        self.fitness = f64::NEG_INFINITY;
187    }
188
189    /// Inversion mutation (reverses a segment).
190    pub fn inversion_mutate<R: Rng>(&mut self, rng: &mut R) {
191        let n = self.order.len();
192        if n < 2 {
193            return;
194        }
195
196        let (mut p1, mut p2) = (rng.random_range(0..n), rng.random_range(0..n));
197        if p1 > p2 {
198            std::mem::swap(&mut p1, &mut p2);
199        }
200
201        self.order[p1..=p2].reverse();
202        self.fitness = f64::NEG_INFINITY;
203    }
204}
205
206impl Individual for NestingChromosome {
207    type Fitness = f64;
208
209    fn fitness(&self) -> f64 {
210        self.fitness
211    }
212
213    fn random<R: Rng>(rng: &mut R) -> Self {
214        // Default: empty, will be overridden by problem's initialize_population
215        Self::random_with_options(0, 1, rng)
216    }
217
218    fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
219        self.order_crossover(other, rng)
220    }
221
222    fn mutate<R: Rng>(&mut self, rng: &mut R) {
223        // 45% swap, 25% inversion, 15% rotation, 15% mirror
224        let r: f64 = rng.random();
225        if r < 0.45 {
226            self.swap_mutate(rng);
227        } else if r < 0.70 {
228            self.inversion_mutate(rng);
229        } else if r < 0.85 {
230            // Rotation mutation with 4 options (0, 90, 180, 270 degrees)
231            self.rotation_mutate(4, rng);
232        } else {
233            self.mirror_mutate(rng);
234        }
235    }
236}
237
238/// Problem definition for GA-based 2D nesting.
239pub struct NestingProblem {
240    /// Input geometries.
241    geometries: Vec<Geometry2D>,
242    /// Boundary container.
243    boundary: Boundary2D,
244    /// Solver configuration.
245    config: Config,
246    /// Instance mapping (instance_id -> (geometry_idx, instance_num)).
247    instances: Vec<InstanceInfo>,
248    /// Available rotation angles per geometry.
249    rotation_angles: Vec<Vec<f64>>,
250    /// Number of rotation options.
251    rotation_options: usize,
252    /// Cancellation flag.
253    cancelled: Arc<AtomicBool>,
254}
255
256impl NestingProblem {
257    /// Creates a new nesting problem.
258    pub fn new(
259        geometries: Vec<Geometry2D>,
260        boundary: Boundary2D,
261        config: Config,
262        cancelled: Arc<AtomicBool>,
263    ) -> Self {
264        // Build instance mapping
265        let mut instances = Vec::new();
266        let mut rotation_angles = Vec::new();
267
268        for (geom_idx, geom) in geometries.iter().enumerate() {
269            // Get rotation angles for this geometry
270            let angles = geom.rotations();
271            let angles = if angles.is_empty() { vec![0.0] } else { angles };
272            rotation_angles.push(angles);
273
274            // Create instances
275            for instance_num in 0..geom.quantity() {
276                instances.push(InstanceInfo {
277                    geometry_idx: geom_idx,
278                    instance_num,
279                });
280            }
281        }
282
283        // Maximum rotation options across all geometries
284        let rotation_options = rotation_angles.iter().map(|a| a.len()).max().unwrap_or(1);
285
286        Self {
287            geometries,
288            boundary,
289            config,
290            instances,
291            rotation_angles,
292            rotation_options,
293            cancelled,
294        }
295    }
296
297    /// Returns the total number of instances.
298    pub fn num_instances(&self) -> usize {
299        self.instances.len()
300    }
301
302    /// Returns the number of rotation options.
303    pub fn rotation_options(&self) -> usize {
304        self.rotation_options
305    }
306
307    /// Decodes a chromosome into placements using NFP-guided placement.
308    pub fn decode(&self, chromosome: &NestingChromosome) -> (Vec<Placement<f64>>, f64, usize) {
309        let mut placements = Vec::new();
310        let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
311        let mut total_placed_area = 0.0;
312        let mut placed_count = 0;
313
314        let margin = self.config.margin;
315        let spacing = self.config.spacing;
316
317        // Get boundary polygon with margin
318        let boundary_polygon = self.get_boundary_polygon_with_margin(margin);
319
320        // Sampling step for grid search
321        let sample_step = self.compute_sample_step();
322
323        // Place geometries in the order specified by chromosome
324        for &instance_idx in chromosome.order.iter() {
325            if self.cancelled.load(Ordering::Relaxed) {
326                break;
327            }
328
329            if instance_idx >= self.instances.len() {
330                continue;
331            }
332
333            let info = &self.instances[instance_idx];
334            let geom = &self.geometries[info.geometry_idx];
335
336            // Get rotation angle from chromosome
337            let rotation_idx = chromosome.rotations.get(instance_idx).copied().unwrap_or(0);
338            let rotation_angle = self
339                .rotation_angles
340                .get(info.geometry_idx)
341                .and_then(|angles| angles.get(rotation_idx % angles.len()))
342                .copied()
343                .unwrap_or(0.0);
344
345            // Mirror flag from chromosome (`allow_flip` support), masked
346            // against this instance's own geometry — see `mirrors` doc comment.
347            let mirror = chromosome
348                .mirrors
349                .get(instance_idx)
350                .copied()
351                .unwrap_or(false)
352                && geom.allow_flip();
353
354            // Compute IFP for this geometry at this rotation
355            let ifp = match compute_ifp_with_margin_and_mirror(
356                &boundary_polygon,
357                geom,
358                rotation_angle,
359                0.0,
360                mirror,
361            ) {
362                Ok(ifp) => ifp,
363                Err(_) => {
364                    continue;
365                }
366            };
367
368            if ifp.is_empty() {
369                continue;
370            }
371
372            // Compute NFPs with all placed geometries
373            let mut nfps: Vec<Nfp> = Vec::new();
374            for placed in &placed_geometries {
375                // Already-mirrored (if applicable) real-world polygon — do
376                // NOT mirror it again below, `mirror_stationary=false` always.
377                let placed_exterior = placed.translated_exterior();
378                let placed_geom = Geometry2D::new(format!("_placed_{}", placed.geometry.id()))
379                    .with_polygon(placed_exterior);
380
381                if let Ok(nfp) =
382                    compute_nfp_mirrored(&placed_geom, geom, rotation_angle, false, mirror)
383                {
384                    let expanded = self.expand_nfp(&nfp, spacing);
385                    nfps.push(expanded);
386                }
387            }
388
389            // Shrink IFP by spacing
390            let ifp_shrunk = self.shrink_ifp(&ifp, spacing);
391
392            // Find the bottom-left valid placement
393            // IFP returns positions where the geometry's origin should be placed.
394            let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
395            let placement_result = find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step);
396            if let Some((x, y)) = placement_result {
397                // Clamp position to keep geometry within boundary
398                // (mirror-aware — an unmirrored AABB has the wrong local
399                // extents for a mirrored candidate, see `aabb_at_rotation_mirrored`).
400                let geom_aabb = geom.aabb_at_rotation_mirrored(rotation_angle, mirror);
401                let boundary_aabb = self.boundary.aabb();
402
403                if let Some((clamped_x, clamped_y)) =
404                    clamp_placement_to_boundary(x, y, geom_aabb, boundary_aabb)
405                {
406                    // Only verify overlap if clamping changed the position
407                    // The original NFP-found position is already collision-free by definition
408                    let was_clamped = (clamped_x - x).abs() > 1e-6 || (clamped_y - y).abs() > 1e-6;
409                    if was_clamped {
410                        // Verify no actual polygon overlap using SAT
411                        if !verify_no_overlap_mirrored(
412                            geom,
413                            (clamped_x, clamped_y),
414                            rotation_angle,
415                            mirror,
416                            &placed_geometries,
417                        ) {
418                            continue; // Skip - clamped position would cause overlap
419                        }
420                    }
421
422                    let placement = Placement::new_2d(
423                        geom.id().clone(),
424                        info.instance_num,
425                        clamped_x,
426                        clamped_y,
427                        rotation_angle,
428                    )
429                    .with_mirrored(mirror);
430
431                    placements.push(placement);
432                    placed_geometries.push(
433                        PlacedGeometry::new(geom.clone(), (clamped_x, clamped_y), rotation_angle)
434                            .with_mirrored(mirror),
435                    );
436                    total_placed_area += geom.measure();
437                    placed_count += 1;
438                }
439            }
440        }
441
442        let utilization = total_placed_area / self.boundary.measure();
443        (placements, utilization, placed_count)
444    }
445
446    /// Gets the boundary polygon with margin applied.
447    fn get_boundary_polygon_with_margin(&self, margin: f64) -> Vec<(f64, f64)> {
448        let (b_min, b_max) = self.boundary.aabb();
449        vec![
450            (b_min[0] + margin, b_min[1] + margin),
451            (b_max[0] - margin, b_min[1] + margin),
452            (b_max[0] - margin, b_max[1] - margin),
453            (b_min[0] + margin, b_max[1] - margin),
454        ]
455    }
456
457    /// Computes an adaptive sample step based on geometry sizes.
458    fn compute_sample_step(&self) -> f64 {
459        if self.geometries.is_empty() {
460            return 1.0;
461        }
462
463        let mut min_dim = f64::INFINITY;
464        for geom in &self.geometries {
465            let (g_min, g_max) = geom.aabb();
466            let width = g_max[0] - g_min[0];
467            let height = g_max[1] - g_min[1];
468            min_dim = min_dim.min(width).min(height);
469        }
470
471        (min_dim / 4.0).clamp(0.5, 10.0)
472    }
473
474    /// Expands an NFP by the given spacing amount.
475    fn expand_nfp(&self, nfp: &Nfp, spacing: f64) -> Nfp {
476        expand_nfp(nfp, spacing)
477    }
478
479    /// Shrinks an IFP by the given spacing amount.
480    fn shrink_ifp(&self, ifp: &Nfp, spacing: f64) -> Nfp {
481        shrink_ifp(ifp, spacing)
482    }
483}
484
485impl GaProblem for NestingProblem {
486    type Individual = NestingChromosome;
487
488    fn evaluate(&self, individual: &mut Self::Individual) {
489        let (_, utilization, placed_count) = self.decode(individual);
490        let fitness = nesting_fitness(placed_count, individual.total_count, utilization);
491        individual.set_fitness(fitness, placed_count);
492    }
493
494    fn initialize_population<R: Rng>(&self, size: usize, rng: &mut R) -> Vec<Self::Individual> {
495        (0..size)
496            .map(|_| {
497                NestingChromosome::random_with_options(
498                    self.num_instances(),
499                    self.rotation_options(),
500                    rng,
501                )
502            })
503            .collect()
504    }
505
506    fn on_generation(
507        &self,
508        generation: u32,
509        best: &Self::Individual,
510        _population: &[Self::Individual],
511    ) {
512        log::debug!(
513            "GA Generation {}: fitness={:.4}, placed={}/{}",
514            generation,
515            best.fitness(),
516            best.placed_count,
517            best.total_count
518        );
519    }
520}
521
522/// Runs GA-based nesting optimization.
523pub fn run_ga_nesting(
524    geometries: &[Geometry2D],
525    boundary: &Boundary2D,
526    config: &Config,
527    ga_config: GaConfig,
528    cancelled: Arc<AtomicBool>,
529) -> SolveResult<f64> {
530    let problem = NestingProblem::new(
531        geometries.to_vec(),
532        boundary.clone(),
533        config.clone(),
534        cancelled.clone(),
535    );
536
537    let runner = GaRunner::new(ga_config, problem);
538
539    // Connect cancellation (thread-based polling, not available on WASM)
540    #[cfg(not(target_arch = "wasm32"))]
541    {
542        let cancel_handle = runner.cancel_handle();
543        let cancelled_clone = cancelled.clone();
544        std::thread::spawn(move || {
545            while !cancelled_clone.load(Ordering::Relaxed) {
546                std::thread::sleep(std::time::Duration::from_millis(100));
547            }
548            cancel_handle.store(true, Ordering::Relaxed);
549        });
550    }
551
552    // Seed the RNG for reproducibility when `config.seed` is set; otherwise use
553    // system entropy (non-deterministic).
554    let ga_result = match config.seed {
555        Some(seed) => runner.run_with_rng(&mut rand::rngs::StdRng::seed_from_u64(seed)),
556        None => runner.run(),
557    };
558
559    // Decode the best chromosome to get final placements
560    let problem = NestingProblem::new(
561        geometries.to_vec(),
562        boundary.clone(),
563        config.clone(),
564        Arc::new(AtomicBool::new(false)),
565    );
566
567    let (placements, utilization, _placed_count) = problem.decode(&ga_result.best);
568
569    // Build unplaced list
570    let mut unplaced = Vec::new();
571    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
572    for p in &placements {
573        placed_ids.insert(p.geometry_id.clone());
574    }
575    for geom in geometries {
576        if !placed_ids.contains(geom.id()) {
577            unplaced.push(geom.id().clone());
578        }
579    }
580
581    let mut result = SolveResult::new();
582    result.placements = placements;
583    result.unplaced = unplaced;
584    result.boundaries_used = 1;
585    result.utilization = utilization;
586    result.computation_time_ms = ga_result.elapsed.as_millis() as u64;
587    result.generations = Some(ga_result.generations);
588    result.best_fitness = Some(ga_result.best.fitness());
589    result.fitness_history = Some(ga_result.history);
590    result.strategy = Some("GeneticAlgorithm".to_string());
591    result.cancelled = cancelled.load(Ordering::Relaxed);
592    result.target_reached = ga_result.target_reached;
593
594    result
595}
596
597/// Runs GA-based nesting optimization with progress callback.
598pub fn run_ga_nesting_with_progress(
599    geometries: &[Geometry2D],
600    boundary: &Boundary2D,
601    config: &Config,
602    ga_config: GaConfig,
603    cancelled: Arc<AtomicBool>,
604    progress_callback: ProgressCallback,
605) -> SolveResult<f64> {
606    let total_items = geometries.iter().map(|g| g.quantity()).sum::<usize>();
607
608    let problem = NestingProblem::new(
609        geometries.to_vec(),
610        boundary.clone(),
611        config.clone(),
612        cancelled.clone(),
613    );
614
615    let runner = GaRunner::new(ga_config.clone(), problem);
616
617    // Connect cancellation (thread-based polling, not available on WASM)
618    #[cfg(not(target_arch = "wasm32"))]
619    {
620        let cancel_handle = runner.cancel_handle();
621        let cancelled_clone = cancelled.clone();
622        std::thread::spawn(move || {
623            while !cancelled_clone.load(Ordering::Relaxed) {
624                std::thread::sleep(std::time::Duration::from_millis(100));
625            }
626            cancel_handle.store(true, Ordering::Relaxed);
627        });
628    }
629
630    // Run GA with progress callback adapter. Thread `config.seed` through so the
631    // callback-driven path is as reproducible as the plain `run_ga_nesting` path
632    // — without this the progress runner fell back to system entropy and a seeded
633    // solve was non-deterministic whenever a progress callback was supplied
634    // (FFI `solve_2d_with_callback`, the WASM/demo path).
635    let max_generations = ga_config.max_generations;
636    let progress_adapter = move |ga_progress: GaProgress<f64>| {
637        let info = ProgressInfo::new()
638            .with_iteration(ga_progress.generation, max_generations)
639            .with_fitness(ga_progress.best_fitness)
640            .with_utilization(ga_progress.best_fitness) // fitness is utilization
641            .with_items(0, total_items) // we don't track placed count during GA
642            .with_elapsed(ga_progress.elapsed.as_millis() as u64)
643            .with_phase("Genetic Algorithm".to_string());
644
645        let info = if !ga_progress.running {
646            info.finished()
647        } else {
648            info
649        };
650
651        progress_callback(info);
652    };
653    let ga_result = match config.seed {
654        Some(seed) => runner.run_with_rng_and_progress(
655            &mut rand::rngs::StdRng::seed_from_u64(seed),
656            Some(progress_adapter),
657        ),
658        None => runner.run_with_progress(progress_adapter),
659    };
660
661    // Decode the best chromosome to get final placements
662    let problem = NestingProblem::new(
663        geometries.to_vec(),
664        boundary.clone(),
665        config.clone(),
666        Arc::new(AtomicBool::new(false)),
667    );
668
669    let (placements, utilization, _placed_count) = problem.decode(&ga_result.best);
670
671    // Build unplaced list
672    let mut unplaced = Vec::new();
673    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
674    for p in &placements {
675        placed_ids.insert(p.geometry_id.clone());
676    }
677    for geom in geometries {
678        if !placed_ids.contains(geom.id()) {
679            unplaced.push(geom.id().clone());
680        }
681    }
682
683    let mut result = SolveResult::new();
684    result.placements = placements;
685    result.unplaced = unplaced;
686    result.boundaries_used = 1;
687    result.utilization = utilization;
688    result.computation_time_ms = ga_result.elapsed.as_millis() as u64;
689    result.generations = Some(ga_result.generations);
690    result.best_fitness = Some(ga_result.best.fitness());
691    result.fitness_history = Some(ga_result.history);
692    result.strategy = Some("GeneticAlgorithm".to_string());
693    result.cancelled = cancelled.load(Ordering::Relaxed);
694    result.target_reached = ga_result.target_reached;
695
696    result
697}
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702
703    #[test]
704    fn test_nesting_chromosome_crossover() {
705        let mut rng = rand::rng();
706        let parent1 = NestingChromosome::random_with_options(10, 4, &mut rng);
707        let parent2 = NestingChromosome::random_with_options(10, 4, &mut rng);
708
709        let child = parent1.order_crossover(&parent2, &mut rng);
710
711        // Child should be a valid permutation
712        assert_eq!(child.order.len(), 10);
713        let mut sorted = child.order.clone();
714        sorted.sort();
715        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
716    }
717
718    #[test]
719    fn test_nesting_chromosome_mutation() {
720        let mut rng = rand::rng();
721        let mut chromosome = NestingChromosome::random_with_options(10, 4, &mut rng);
722
723        chromosome.swap_mutate(&mut rng);
724
725        // Should still be a valid permutation
726        let mut sorted = chromosome.order.clone();
727        sorted.sort();
728        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
729    }
730
731    #[test]
732    fn test_nesting_chromosome_mirrors_gene_present() {
733        let mut rng = rand::rng();
734        let chromosome = NestingChromosome::random_with_options(10, 4, &mut rng);
735        assert_eq!(chromosome.mirrors.len(), 10);
736
737        let fixed = NestingChromosome::new(10, 4);
738        assert_eq!(fixed.mirrors, vec![false; 10]);
739    }
740
741    #[test]
742    fn test_nesting_chromosome_mirror_crossover() {
743        let mut rng = rand::rng();
744        // All-true vs all-false parents — every crossover gene must come
745        // from exactly one of the two, so the child is either true or false
746        // per position (not some third corrupted value — trivially true for
747        // bool, but this also proves the vector is populated per-position,
748        // not left at a stale default length).
749        let mut parent1 = NestingChromosome::random_with_options(10, 4, &mut rng);
750        let mut parent2 = NestingChromosome::random_with_options(10, 4, &mut rng);
751        parent1.mirrors = vec![true; 10];
752        parent2.mirrors = vec![false; 10];
753
754        let child = parent1.order_crossover(&parent2, &mut rng);
755        assert_eq!(child.mirrors.len(), 10);
756        assert!(child.mirrors.iter().all(|&m| m || !m)); // always true for bool; length/no-panic is the real assertion
757    }
758
759    #[test]
760    fn test_mirror_mutate_flips_bit() {
761        let mut rng = rand::rng();
762        let mut chromosome = NestingChromosome::new(5, 1);
763        assert_eq!(chromosome.mirrors, vec![false; 5]);
764
765        // Deterministic single-instance chromosome — flip must change it.
766        let mut single = NestingChromosome::new(1, 1);
767        single.mirror_mutate(&mut rng);
768        assert!(single.mirrors[0]);
769        single.mirror_mutate(&mut rng);
770        assert!(!single.mirrors[0]);
771
772        chromosome.mirror_mutate(&mut rng);
773        assert_eq!(chromosome.mirrors.iter().filter(|&&m| m).count(), 1);
774    }
775
776    /// Chiral L-shape — see `nfp.rs`'s `chiral_l` fixture for why this
777    /// specific shape (asymmetric width/height/notch, no reflection symmetry).
778    fn chiral_l(id: &str) -> Geometry2D {
779        Geometry2D::l_shape(id, 30.0, 20.0, 20.0, 10.0)
780    }
781
782    fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
783        for i in 0..a.len() {
784            let (a1, a2) = (a[i], a[(i + 1) % a.len()]);
785            for j in 0..b.len() {
786                let (b1, b2) = (b[j], b[(j + 1) % b.len()]);
787                if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
788                    return true;
789                }
790            }
791        }
792        false
793    }
794
795    /// Phase 2 (`allow_flip`/mirroring), GA strategy. Unlike BLF
796    /// (`nester.rs`), `NestingProblem::decode()` calls no `.validate()` at
797    /// all (only `solve()`'s centralized `validate_geometries` gate does),
798    /// so calling it directly exercises the mirror gene deterministically —
799    /// useful even now that the public gate is open (Phase 4), since the
800    /// GA's own public path is randomized.
801    #[test]
802    fn test_ga_decode_mirror_no_overlap() {
803        let geometries = vec![chiral_l("L").with_flip(true).with_quantity(2)];
804        let boundary = Boundary2D::rectangle(65.0, 45.0);
805        let config = Config::default().with_spacing(1.0);
806        let problem = NestingProblem::new(
807            geometries.clone(),
808            boundary,
809            config,
810            Arc::new(AtomicBool::new(false)),
811        );
812
813        // Chromosome the GA could produce: place instance 0 first
814        // unmirrored, instance 1 second mirrored — decode() applies exactly
815        // what the chromosome specifies (the GA's job across generations is
816        // to discover which values are good, not decode()'s).
817        let mut chromosome = NestingChromosome::new(2, 1);
818        chromosome.mirrors = vec![false, true];
819
820        let (placements, utilization, placed_count) = problem.decode(&chromosome);
821
822        assert_eq!(
823            placed_count, 2,
824            "both instances should fit in this boundary"
825        );
826        assert_eq!(placements.len(), 2);
827        assert!(utilization > 0.0);
828        assert!(!placements[0].mirrored);
829        assert!(
830            placements[1].mirrored,
831            "instance 1's mirror gene was true and allow_flip is set — decode() must honor it"
832        );
833
834        let poly0 = PlacedGeometry::new(
835            geometries[0].clone(),
836            (placements[0].x(), placements[0].y()),
837            placements[0].angle(),
838        )
839        .with_mirrored(placements[0].mirrored)
840        .translated_exterior();
841        let poly1 = PlacedGeometry::new(
842            geometries[0].clone(),
843            (placements[1].x(), placements[1].y()),
844            placements[1].angle(),
845        )
846        .with_mirrored(placements[1].mirrored)
847        .translated_exterior();
848        assert!(
849            !polygons_overlap(&poly0, &poly1),
850            "unmirrored instance 0 and mirrored instance 1 must not overlap"
851        );
852    }
853
854    #[test]
855    fn test_ga_decode_mirror_ignored_without_allow_flip() {
856        // allow_flip defaults to false: even a true mirror gene must be
857        // masked off by decode() (`&& geom.allow_flip()`), not honored.
858        let geometries = vec![chiral_l("L").with_quantity(1)];
859        let boundary = Boundary2D::rectangle(65.0, 45.0);
860        let problem = NestingProblem::new(
861            geometries,
862            boundary,
863            Config::default(),
864            Arc::new(AtomicBool::new(false)),
865        );
866
867        let mut chromosome = NestingChromosome::new(1, 1);
868        chromosome.mirrors = vec![true];
869
870        let (placements, _utilization, placed_count) = problem.decode(&chromosome);
871        assert_eq!(placed_count, 1);
872        assert!(
873            !placements[0].mirrored,
874            "allow_flip=false must suppress the mirror gene"
875        );
876    }
877
878    #[test]
879    fn test_ga_nesting_basic() {
880        let geometries = vec![
881            Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
882            Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
883        ];
884
885        let boundary = Boundary2D::rectangle(100.0, 50.0);
886        let config = Config::default();
887        let ga_config = GaConfig::default()
888            .with_population_size(20)
889            .with_max_generations(10);
890
891        let result = run_ga_nesting(
892            &geometries,
893            &boundary,
894            &config,
895            ga_config,
896            Arc::new(AtomicBool::new(false)),
897        );
898
899        assert!(result.utilization > 0.0);
900        assert!(!result.placements.is_empty());
901    }
902
903    #[test]
904    fn test_ga_nesting_all_placed() {
905        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
906
907        let boundary = Boundary2D::rectangle(100.0, 100.0);
908        let config = Config::default();
909        let ga_config = GaConfig::default()
910            .with_population_size(30)
911            .with_max_generations(20);
912
913        let result = run_ga_nesting(
914            &geometries,
915            &boundary,
916            &config,
917            ga_config,
918            Arc::new(AtomicBool::new(false)),
919        );
920
921        // All 4 pieces should fit easily
922        assert_eq!(result.placements.len(), 4);
923        assert!(result.unplaced.is_empty());
924    }
925
926    #[test]
927    fn test_ga_nesting_with_rotation() {
928        // L-shaped pieces that might benefit from rotation
929        let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
930            .with_quantity(3)
931            .with_rotations(vec![0.0, 90.0])];
932
933        let boundary = Boundary2D::rectangle(50.0, 50.0);
934        let config = Config::default();
935        let ga_config = GaConfig::default()
936            .with_population_size(30)
937            .with_max_generations(20);
938
939        let result = run_ga_nesting(
940            &geometries,
941            &boundary,
942            &config,
943            ga_config,
944            Arc::new(AtomicBool::new(false)),
945        );
946
947        assert!(result.utilization > 0.0);
948        // Should be able to place at least some pieces
949        assert!(!result.placements.is_empty());
950    }
951
952    #[test]
953    fn test_nesting_problem_decode() {
954        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2)];
955
956        let boundary = Boundary2D::rectangle(100.0, 50.0);
957        let config = Config::default();
958        let cancelled = Arc::new(AtomicBool::new(false));
959
960        let problem = NestingProblem::new(geometries, boundary, config, cancelled);
961
962        assert_eq!(problem.num_instances(), 2);
963
964        // Create a chromosome and decode
965        let chromosome = NestingChromosome::new(2, 1);
966        let (placements, utilization, placed_count) = problem.decode(&chromosome);
967
968        assert_eq!(placed_count, 2);
969        assert_eq!(placements.len(), 2);
970        assert!(utilization > 0.0);
971    }
972}