Skip to main content

u_nesting_d2/
brkga_nesting.rs

1//! BRKGA-based 2D nesting optimization.
2//!
3//! This module provides BRKGA (Biased Random-Key Genetic Algorithm) based
4//! optimization for 2D nesting problems. BRKGA uses random-key encoding
5//! and biased crossover to favor elite parents.
6//!
7//! # Random-Key Encoding
8//!
9//! Each solution is encoded as a vector of random keys in [0, 1):
10//! - First N keys: decoded as permutation (placement order)
11//! - Next N keys: decoded as rotation indices
12//!
13//! # Reference
14//!
15//! Gonçalves, J. F., & Resende, M. G. (2013). A biased random key genetic
16//! algorithm for 2D and 3D bin packing problems.
17
18use crate::boundary::Boundary2D;
19use crate::clamp_placement_to_boundary;
20use crate::geometry::Geometry2D;
21use crate::nfp::{
22    compute_ifp_with_margin_and_mirror, compute_nfp_mirrored, find_bottom_left_placement,
23    verify_no_overlap_mirrored, Nfp, PlacedGeometry,
24};
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::Arc;
27use u_nesting_core::brkga::{BrkgaConfig, BrkgaProblem, BrkgaRunner, RandomKeyChromosome};
28use u_nesting_core::geometry::{Boundary, Geometry};
29use u_nesting_core::solver::Config;
30use u_nesting_core::{Placement, SolveResult};
31
32use crate::placement_utils::{expand_nfp, nesting_fitness, shrink_ifp, InstanceInfo};
33
34/// BRKGA problem definition for 2D nesting.
35pub struct BrkgaNestingProblem {
36    /// Input geometries.
37    geometries: Vec<Geometry2D>,
38    /// Boundary container.
39    boundary: Boundary2D,
40    /// Solver configuration.
41    config: Config,
42    /// Instance mapping (instance_id -> (geometry_idx, instance_num)).
43    instances: Vec<InstanceInfo>,
44    /// Available rotation angles per geometry.
45    rotation_angles: Vec<Vec<f64>>,
46    /// Whether any geometry allows mirroring (`allow_flip` support) — gates
47    /// whether the chromosome carries a third key block for mirror flags.
48    any_allow_flip: bool,
49    /// Cancellation flag.
50    cancelled: Arc<AtomicBool>,
51}
52
53impl BrkgaNestingProblem {
54    /// Creates a new BRKGA nesting problem.
55    pub fn new(
56        geometries: Vec<Geometry2D>,
57        boundary: Boundary2D,
58        config: Config,
59        cancelled: Arc<AtomicBool>,
60    ) -> Self {
61        // Build instance mapping
62        let mut instances = Vec::new();
63        let mut rotation_angles = Vec::new();
64        let mut any_allow_flip = false;
65
66        for (geom_idx, geom) in geometries.iter().enumerate() {
67            // Get rotation angles for this geometry
68            let angles = geom.rotations();
69            let angles = if angles.is_empty() { vec![0.0] } else { angles };
70            rotation_angles.push(angles);
71            any_allow_flip = any_allow_flip || geom.allow_flip();
72
73            // Create instances
74            for instance_num in 0..geom.quantity() {
75                instances.push(InstanceInfo {
76                    geometry_idx: geom_idx,
77                    instance_num,
78                });
79            }
80        }
81
82        Self {
83            geometries,
84            boundary,
85            config,
86            instances,
87            rotation_angles,
88            any_allow_flip,
89            cancelled,
90        }
91    }
92
93    /// Returns the total number of instances.
94    pub fn num_instances(&self) -> usize {
95        self.instances.len()
96    }
97
98    /// Decodes a chromosome into placements using NFP-guided placement.
99    ///
100    /// The chromosome keys are interpreted as:
101    /// - Keys [0..N): placement order (sorted indices)
102    /// - Keys [N..2N): rotation indices (discretized)
103    /// - Keys [2N..3N), only when `any_allow_flip`: mirror flags
104    ///   (`allow_flip` support, discretized to 2 options) — a third block,
105    ///   same encoding style as rotation, only present when at least one
106    ///   geometry can use it (keeps the chromosome at its original 2N length
107    ///   for problems that never need mirroring).
108    pub fn decode(&self, chromosome: &RandomKeyChromosome) -> (Vec<Placement<f64>>, f64, usize) {
109        let n = self.instances.len();
110        if n == 0 || chromosome.len() < n {
111            return (Vec::new(), 0.0, 0);
112        }
113
114        // Decode placement order from first N keys
115        let order = chromosome.decode_as_permutation();
116        // Only take first N indices (in case chromosome has extra keys)
117        let order: Vec<usize> = order.into_iter().take(n).collect();
118
119        let mut placements = Vec::new();
120        let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
121        let mut total_placed_area = 0.0;
122        let mut placed_count = 0;
123
124        let margin = self.config.margin;
125        let spacing = self.config.spacing;
126
127        // Get boundary polygon with margin
128        let boundary_polygon = self.get_boundary_polygon_with_margin(margin);
129
130        // Sampling step for grid search
131        let sample_step = self.compute_sample_step();
132
133        // Place geometries in the decoded order
134        for &instance_idx in &order {
135            if self.cancelled.load(Ordering::Relaxed) {
136                break;
137            }
138
139            if instance_idx >= self.instances.len() {
140                continue;
141            }
142
143            let info = &self.instances[instance_idx];
144            let geom = &self.geometries[info.geometry_idx];
145
146            // Decode rotation from the second half of keys
147            let rotation_key_idx = n + instance_idx;
148            let num_rotations = self
149                .rotation_angles
150                .get(info.geometry_idx)
151                .map(|a| a.len())
152                .unwrap_or(1);
153
154            let rotation_idx = if rotation_key_idx < chromosome.len() {
155                chromosome.decode_as_discrete(rotation_key_idx, num_rotations)
156            } else {
157                0
158            };
159
160            let rotation_angle = self
161                .rotation_angles
162                .get(info.geometry_idx)
163                .and_then(|angles| angles.get(rotation_idx))
164                .copied()
165                .unwrap_or(0.0);
166
167            // Decode mirror flag from the third key block (`allow_flip` support).
168            let mirror_key_idx = 2 * n + instance_idx;
169            let mirror = self.any_allow_flip
170                && mirror_key_idx < chromosome.len()
171                && chromosome.decode_as_discrete(mirror_key_idx, 2) == 1
172                && geom.allow_flip();
173
174            // Compute IFP for this geometry at this rotation
175            let ifp = match compute_ifp_with_margin_and_mirror(
176                &boundary_polygon,
177                geom,
178                rotation_angle,
179                0.0,
180                mirror,
181            ) {
182                Ok(ifp) => ifp,
183                Err(_) => continue,
184            };
185
186            if ifp.is_empty() {
187                continue;
188            }
189
190            // Compute NFPs with all placed geometries
191            let mut nfps: Vec<Nfp> = Vec::new();
192            for placed in &placed_geometries {
193                // Already-mirrored (if applicable) real-world polygon — do
194                // NOT mirror it again below, `mirror_stationary=false` always.
195                let placed_exterior = placed.translated_exterior();
196                let placed_geom = Geometry2D::new(format!("_placed_{}", placed.geometry.id()))
197                    .with_polygon(placed_exterior);
198
199                if let Ok(nfp) =
200                    compute_nfp_mirrored(&placed_geom, geom, rotation_angle, false, mirror)
201                {
202                    let expanded = self.expand_nfp(&nfp, spacing);
203                    nfps.push(expanded);
204                }
205            }
206
207            // Shrink IFP by spacing
208            let ifp_shrunk = self.shrink_ifp(&ifp, spacing);
209
210            // Find the bottom-left valid placement
211            // IFP returns positions where the geometry's origin should be placed.
212            // Clamp to ensure placement keeps geometry within boundary.
213            let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
214            if let Some((x, y)) = find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step) {
215                // Clamp position to keep geometry within boundary
216                // (mirror-aware — an unmirrored AABB has the wrong local
217                // extents for a mirrored candidate, see `aabb_at_rotation_mirrored`).
218                let geom_aabb = geom.aabb_at_rotation_mirrored(rotation_angle, mirror);
219                let boundary_aabb = self.boundary.aabb();
220
221                if let Some((clamped_x, clamped_y)) =
222                    clamp_placement_to_boundary(x, y, geom_aabb, boundary_aabb)
223                {
224                    // Only verify overlap if clamping changed the position
225                    // The original NFP-found position is already collision-free by definition
226                    let was_clamped = (clamped_x - x).abs() > 1e-6 || (clamped_y - y).abs() > 1e-6;
227                    if was_clamped {
228                        // Verify no actual polygon overlap using SAT
229                        if !verify_no_overlap_mirrored(
230                            geom,
231                            (clamped_x, clamped_y),
232                            rotation_angle,
233                            mirror,
234                            &placed_geometries,
235                        ) {
236                            continue; // Skip - clamped position would cause overlap
237                        }
238                    }
239
240                    let placement = Placement::new_2d(
241                        geom.id().clone(),
242                        info.instance_num,
243                        clamped_x,
244                        clamped_y,
245                        rotation_angle,
246                    )
247                    .with_mirrored(mirror);
248
249                    placements.push(placement);
250                    placed_geometries.push(
251                        PlacedGeometry::new(geom.clone(), (clamped_x, clamped_y), rotation_angle)
252                            .with_mirrored(mirror),
253                    );
254                    total_placed_area += geom.measure();
255                    placed_count += 1;
256                }
257            }
258        }
259
260        let utilization = total_placed_area / self.boundary.measure();
261        (placements, utilization, placed_count)
262    }
263
264    /// Gets the boundary polygon with margin applied.
265    fn get_boundary_polygon_with_margin(&self, margin: f64) -> Vec<(f64, f64)> {
266        let (b_min, b_max) = self.boundary.aabb();
267        vec![
268            (b_min[0] + margin, b_min[1] + margin),
269            (b_max[0] - margin, b_min[1] + margin),
270            (b_max[0] - margin, b_max[1] - margin),
271            (b_min[0] + margin, b_max[1] - margin),
272        ]
273    }
274
275    /// Computes an adaptive sample step based on geometry sizes.
276    fn compute_sample_step(&self) -> f64 {
277        if self.geometries.is_empty() {
278            return 1.0;
279        }
280
281        let mut min_dim = f64::INFINITY;
282        for geom in &self.geometries {
283            let (g_min, g_max) = geom.aabb();
284            let width = g_max[0] - g_min[0];
285            let height = g_max[1] - g_min[1];
286            min_dim = min_dim.min(width).min(height);
287        }
288
289        (min_dim / 4.0).clamp(0.5, 10.0)
290    }
291
292    /// Expands an NFP by the given spacing amount.
293    fn expand_nfp(&self, nfp: &Nfp, spacing: f64) -> Nfp {
294        expand_nfp(nfp, spacing)
295    }
296
297    /// Shrinks an IFP by the given spacing amount.
298    fn shrink_ifp(&self, ifp: &Nfp, spacing: f64) -> Nfp {
299        shrink_ifp(ifp, spacing)
300    }
301}
302
303impl BrkgaProblem for BrkgaNestingProblem {
304    fn num_keys(&self) -> usize {
305        // N keys for order + N keys for rotations + (if any_allow_flip) N
306        // keys for mirror flags.
307        let n = self.instances.len();
308        if self.any_allow_flip {
309            n * 3
310        } else {
311            n * 2
312        }
313    }
314
315    fn evaluate(&self, chromosome: &mut RandomKeyChromosome) {
316        let (_, utilization, placed_count) = self.decode(chromosome);
317        let fitness = nesting_fitness(placed_count, self.instances.len(), utilization);
318        chromosome.set_fitness(fitness);
319    }
320
321    fn on_generation(
322        &self,
323        generation: u32,
324        best: &RandomKeyChromosome,
325        _population: &[RandomKeyChromosome],
326    ) {
327        log::debug!(
328            "BRKGA Generation {}: fitness={:.4}",
329            generation,
330            best.fitness()
331        );
332    }
333}
334
335/// Runs BRKGA-based nesting optimization.
336pub fn run_brkga_nesting(
337    geometries: &[Geometry2D],
338    boundary: &Boundary2D,
339    config: &Config,
340    brkga_config: BrkgaConfig,
341    cancelled: Arc<AtomicBool>,
342) -> SolveResult<f64> {
343    let problem = BrkgaNestingProblem::new(
344        geometries.to_vec(),
345        boundary.clone(),
346        config.clone(),
347        cancelled.clone(),
348    );
349
350    let runner = BrkgaRunner::with_cancellation(brkga_config, problem, cancelled.clone());
351
352    // Seed the RNG for reproducibility when `config.seed` is set; otherwise use
353    // system entropy (non-deterministic).
354    let brkga_result = match config.seed {
355        Some(seed) => {
356            use rand::SeedableRng;
357            runner.run_with_rng(&mut rand::rngs::StdRng::seed_from_u64(seed))
358        }
359        None => runner.run(),
360    };
361
362    // Decode the best chromosome to get final placements
363    let problem = BrkgaNestingProblem::new(
364        geometries.to_vec(),
365        boundary.clone(),
366        config.clone(),
367        Arc::new(AtomicBool::new(false)),
368    );
369
370    let (placements, utilization, _placed_count) = problem.decode(&brkga_result.best);
371
372    // Build unplaced list
373    let mut unplaced = Vec::new();
374    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
375    for p in &placements {
376        placed_ids.insert(p.geometry_id.clone());
377    }
378    for geom in geometries {
379        if !placed_ids.contains(geom.id()) {
380            unplaced.push(geom.id().clone());
381        }
382    }
383
384    let mut result = SolveResult::new();
385    result.placements = placements;
386    result.unplaced = unplaced;
387    result.boundaries_used = 1;
388    result.utilization = utilization;
389    result.computation_time_ms = brkga_result.elapsed.as_millis() as u64;
390    result.generations = Some(brkga_result.generations);
391    result.best_fitness = Some(brkga_result.best.fitness());
392    result.fitness_history = Some(brkga_result.history);
393    result.strategy = Some("BRKGA".to_string());
394    result.cancelled = cancelled.load(Ordering::Relaxed);
395    result.target_reached = brkga_result.target_reached;
396
397    result
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn test_brkga_nesting_basic() {
406        let geometries = vec![
407            Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
408            Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
409        ];
410
411        let boundary = Boundary2D::rectangle(100.0, 50.0);
412        let config = Config::default();
413        let brkga_config = BrkgaConfig::default()
414            .with_population_size(30)
415            .with_max_generations(20);
416
417        let result = run_brkga_nesting(
418            &geometries,
419            &boundary,
420            &config,
421            brkga_config,
422            Arc::new(AtomicBool::new(false)),
423        );
424
425        assert!(result.utilization > 0.0);
426        assert!(!result.placements.is_empty());
427        assert_eq!(result.strategy, Some("BRKGA".to_string()));
428    }
429
430    #[test]
431    fn test_brkga_nesting_all_placed() {
432        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
433
434        let boundary = Boundary2D::rectangle(100.0, 100.0);
435        let config = Config::default();
436        let brkga_config = BrkgaConfig::default()
437            .with_population_size(30)
438            .with_max_generations(30);
439
440        let result = run_brkga_nesting(
441            &geometries,
442            &boundary,
443            &config,
444            brkga_config,
445            Arc::new(AtomicBool::new(false)),
446        );
447
448        // All 4 pieces should fit easily
449        assert_eq!(result.placements.len(), 4);
450        assert!(result.unplaced.is_empty());
451    }
452
453    #[test]
454    fn test_brkga_nesting_with_rotation() {
455        let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
456            .with_quantity(3)
457            .with_rotations(vec![0.0, 90.0])];
458
459        let boundary = Boundary2D::rectangle(50.0, 50.0);
460        let config = Config::default();
461        let brkga_config = BrkgaConfig::default()
462            .with_population_size(30)
463            .with_max_generations(30);
464
465        let result = run_brkga_nesting(
466            &geometries,
467            &boundary,
468            &config,
469            brkga_config,
470            Arc::new(AtomicBool::new(false)),
471        );
472
473        assert!(result.utilization > 0.0);
474        assert!(!result.placements.is_empty());
475    }
476
477    #[test]
478    fn test_brkga_problem_decode() {
479        use rand::SeedableRng;
480
481        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2)];
482
483        let boundary = Boundary2D::rectangle(100.0, 50.0);
484        let config = Config::default();
485        let cancelled = Arc::new(AtomicBool::new(false));
486
487        let problem = BrkgaNestingProblem::new(geometries, boundary, config, cancelled);
488
489        assert_eq!(problem.num_instances(), 2);
490        // 2 instances * 2 (order + rotation) = 4 keys
491        assert_eq!(problem.num_keys(), 4);
492
493        // Create a chromosome with fixed seed for deterministic test
494        let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
495        let chromosome = RandomKeyChromosome::random(problem.num_keys(), &mut rng);
496        let (placements, utilization, placed_count) = problem.decode(&chromosome);
497
498        // Decoding should produce valid output (may or may not place items depending on random keys)
499        assert_eq!(placements.len(), placed_count);
500        if placed_count > 0 {
501            assert!(utilization > 0.0);
502        }
503    }
504
505    #[test]
506    fn test_brkga_num_keys_includes_mirror_block_only_when_allowed() {
507        let boundary = Boundary2D::rectangle(65.0, 45.0);
508        let cancelled = Arc::new(AtomicBool::new(false));
509
510        let plain = vec![Geometry2D::rectangle("R", 10.0, 10.0).with_quantity(2)];
511        let problem = BrkgaNestingProblem::new(
512            plain,
513            boundary.clone(),
514            Config::default(),
515            cancelled.clone(),
516        );
517        assert_eq!(
518            problem.num_keys(),
519            4,
520            "2 instances * 2 blocks (order + rotation)"
521        );
522
523        let flippable = vec![Geometry2D::rectangle("R", 10.0, 10.0)
524            .with_flip(true)
525            .with_quantity(2)];
526        let problem = BrkgaNestingProblem::new(flippable, boundary, Config::default(), cancelled);
527        assert_eq!(
528            problem.num_keys(),
529            6,
530            "2 instances * 3 blocks (order + rotation + mirror)"
531        );
532    }
533
534    /// Chiral L-shape — see `nfp.rs`'s `chiral_l` fixture for why this
535    /// specific shape (asymmetric width/height/notch, no reflection symmetry).
536    fn chiral_l(id: &str) -> Geometry2D {
537        Geometry2D::l_shape(id, 30.0, 20.0, 20.0, 10.0)
538    }
539
540    fn polygons_overlap(a: &[(f64, f64)], b: &[(f64, f64)]) -> bool {
541        for i in 0..a.len() {
542            let (a1, a2) = (a[i], a[(i + 1) % a.len()]);
543            for j in 0..b.len() {
544                let (b1, b2) = (b[j], b[(j + 1) % b.len()]);
545                if crate::polygon_ops::segments_intersect(a1, a2, b1, b2) {
546                    return true;
547                }
548            }
549        }
550        false
551    }
552
553    /// Phase 4-item (`allow_flip`/mirroring), BRKGA strategy. Same bypass
554    /// strategy as GA/SA: `decode()` calls no `.validate()` at all.
555    ///
556    /// Chromosome keys are hand-picked (not random) to force a deterministic
557    /// order/mirror outcome: `decode_as_permutation()` sorts ALL `num_keys()`
558    /// key indices by value (a pre-existing property of this decoder, not
559    /// something this change introduces — see the third key block's doc
560    /// comment), so the order-block keys (indices 0, 1) are set smaller than
561    /// every other key to guarantee they sort first and `.take(n)` selects
562    /// exactly instances 0 and 1, in that order.
563    #[test]
564    fn test_brkga_decode_mirror_no_overlap() {
565        let geometries = vec![chiral_l("L").with_flip(true).with_quantity(2)];
566        let boundary = Boundary2D::rectangle(65.0, 45.0);
567        let config = Config::default().with_spacing(1.0);
568        let problem = BrkgaNestingProblem::new(
569            geometries.clone(),
570            boundary,
571            config,
572            Arc::new(AtomicBool::new(false)),
573        );
574        assert_eq!(problem.num_keys(), 6);
575
576        let mut chromosome = RandomKeyChromosome::new(6);
577        chromosome.keys = vec![
578            0.01, // order: instance 0 first
579            0.02, // order: instance 1 second
580            0.5, 0.5,  // rotation (only 1 option here, value irrelevant)
581            0.25, // mirror: instance 0 -> false
582            0.75, // mirror: instance 1 -> true
583        ];
584
585        let (placements, utilization, placed_count) = problem.decode(&chromosome);
586
587        assert_eq!(
588            placed_count, 2,
589            "both instances should fit in this boundary"
590        );
591        assert_eq!(placements.len(), 2);
592        assert!(utilization > 0.0);
593        assert!(!placements[0].mirrored);
594        assert!(
595            placements[1].mirrored,
596            "instance 1's mirror key decoded to true and allow_flip is set — decode() must honor it"
597        );
598
599        let poly0 = PlacedGeometry::new(
600            geometries[0].clone(),
601            (placements[0].x(), placements[0].y()),
602            placements[0].angle(),
603        )
604        .with_mirrored(placements[0].mirrored)
605        .translated_exterior();
606        let poly1 = PlacedGeometry::new(
607            geometries[0].clone(),
608            (placements[1].x(), placements[1].y()),
609            placements[1].angle(),
610        )
611        .with_mirrored(placements[1].mirrored)
612        .translated_exterior();
613        assert!(
614            !polygons_overlap(&poly0, &poly1),
615            "unmirrored instance 0 and mirrored instance 1 must not overlap"
616        );
617    }
618
619    #[test]
620    fn test_brkga_decode_mirror_ignored_without_allow_flip() {
621        let geometries = vec![chiral_l("L").with_quantity(1)];
622        let boundary = Boundary2D::rectangle(65.0, 45.0);
623        let problem = BrkgaNestingProblem::new(
624            geometries,
625            boundary,
626            Config::default(),
627            Arc::new(AtomicBool::new(false)),
628        );
629        // allow_flip=false -> num_keys stays at the 2-block size, no mirror
630        // block at all.
631        assert_eq!(problem.num_keys(), 2);
632
633        let mut chromosome = RandomKeyChromosome::new(2);
634        chromosome.keys = vec![0.01, 0.5];
635
636        let (placements, _utilization, placed_count) = problem.decode(&chromosome);
637        assert_eq!(placed_count, 1);
638        assert!(
639            !placements[0].mirrored,
640            "allow_flip=false must suppress mirroring"
641        );
642    }
643}