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, compute_nfp, find_bottom_left_placement, verify_no_overlap, Nfp, PlacedGeometry,
23};
24use std::sync::atomic::{AtomicBool, Ordering};
25use std::sync::Arc;
26use u_nesting_core::brkga::{BrkgaConfig, BrkgaProblem, BrkgaRunner, RandomKeyChromosome};
27use u_nesting_core::geometry::{Boundary, Geometry};
28use u_nesting_core::solver::Config;
29use u_nesting_core::{Placement, SolveResult};
30
31use crate::placement_utils::{expand_nfp, nesting_fitness, shrink_ifp, InstanceInfo};
32
33/// BRKGA problem definition for 2D nesting.
34pub struct BrkgaNestingProblem {
35    /// Input geometries.
36    geometries: Vec<Geometry2D>,
37    /// Boundary container.
38    boundary: Boundary2D,
39    /// Solver configuration.
40    config: Config,
41    /// Instance mapping (instance_id -> (geometry_idx, instance_num)).
42    instances: Vec<InstanceInfo>,
43    /// Available rotation angles per geometry.
44    rotation_angles: Vec<Vec<f64>>,
45    /// Cancellation flag.
46    cancelled: Arc<AtomicBool>,
47}
48
49impl BrkgaNestingProblem {
50    /// Creates a new BRKGA nesting problem.
51    pub fn new(
52        geometries: Vec<Geometry2D>,
53        boundary: Boundary2D,
54        config: Config,
55        cancelled: Arc<AtomicBool>,
56    ) -> Self {
57        // Build instance mapping
58        let mut instances = Vec::new();
59        let mut rotation_angles = Vec::new();
60
61        for (geom_idx, geom) in geometries.iter().enumerate() {
62            // Get rotation angles for this geometry
63            let angles = geom.rotations();
64            let angles = if angles.is_empty() { vec![0.0] } else { angles };
65            rotation_angles.push(angles);
66
67            // Create instances
68            for instance_num in 0..geom.quantity() {
69                instances.push(InstanceInfo {
70                    geometry_idx: geom_idx,
71                    instance_num,
72                });
73            }
74        }
75
76        Self {
77            geometries,
78            boundary,
79            config,
80            instances,
81            rotation_angles,
82            cancelled,
83        }
84    }
85
86    /// Returns the total number of instances.
87    pub fn num_instances(&self) -> usize {
88        self.instances.len()
89    }
90
91    /// Decodes a chromosome into placements using NFP-guided placement.
92    ///
93    /// The chromosome keys are interpreted as:
94    /// - Keys [0..N): placement order (sorted indices)
95    /// - Keys [N..2N): rotation indices (discretized)
96    pub fn decode(&self, chromosome: &RandomKeyChromosome) -> (Vec<Placement<f64>>, f64, usize) {
97        let n = self.instances.len();
98        if n == 0 || chromosome.len() < n {
99            return (Vec::new(), 0.0, 0);
100        }
101
102        // Decode placement order from first N keys
103        let order = chromosome.decode_as_permutation();
104        // Only take first N indices (in case chromosome has extra keys)
105        let order: Vec<usize> = order.into_iter().take(n).collect();
106
107        let mut placements = Vec::new();
108        let mut placed_geometries: Vec<PlacedGeometry> = Vec::new();
109        let mut total_placed_area = 0.0;
110        let mut placed_count = 0;
111
112        let margin = self.config.margin;
113        let spacing = self.config.spacing;
114
115        // Get boundary polygon with margin
116        let boundary_polygon = self.get_boundary_polygon_with_margin(margin);
117
118        // Sampling step for grid search
119        let sample_step = self.compute_sample_step();
120
121        // Place geometries in the decoded order
122        for &instance_idx in &order {
123            if self.cancelled.load(Ordering::Relaxed) {
124                break;
125            }
126
127            if instance_idx >= self.instances.len() {
128                continue;
129            }
130
131            let info = &self.instances[instance_idx];
132            let geom = &self.geometries[info.geometry_idx];
133
134            // Decode rotation from the second half of keys
135            let rotation_key_idx = n + instance_idx;
136            let num_rotations = self
137                .rotation_angles
138                .get(info.geometry_idx)
139                .map(|a| a.len())
140                .unwrap_or(1);
141
142            let rotation_idx = if rotation_key_idx < chromosome.len() {
143                chromosome.decode_as_discrete(rotation_key_idx, num_rotations)
144            } else {
145                0
146            };
147
148            let rotation_angle = self
149                .rotation_angles
150                .get(info.geometry_idx)
151                .and_then(|angles| angles.get(rotation_idx))
152                .copied()
153                .unwrap_or(0.0);
154
155            // Compute IFP for this geometry at this rotation
156            let ifp = match compute_ifp(&boundary_polygon, geom, rotation_angle) {
157                Ok(ifp) => ifp,
158                Err(_) => continue,
159            };
160
161            if ifp.is_empty() {
162                continue;
163            }
164
165            // Compute NFPs with all placed geometries
166            let mut nfps: Vec<Nfp> = Vec::new();
167            for placed in &placed_geometries {
168                let placed_exterior = placed.translated_exterior();
169                let placed_geom = Geometry2D::new(format!("_placed_{}", placed.geometry.id()))
170                    .with_polygon(placed_exterior);
171
172                if let Ok(nfp) = compute_nfp(&placed_geom, geom, rotation_angle) {
173                    let expanded = self.expand_nfp(&nfp, spacing);
174                    nfps.push(expanded);
175                }
176            }
177
178            // Shrink IFP by spacing
179            let ifp_shrunk = self.shrink_ifp(&ifp, spacing);
180
181            // Find the bottom-left valid placement
182            // IFP returns positions where the geometry's origin should be placed.
183            // Clamp to ensure placement keeps geometry within boundary.
184            let nfp_refs: Vec<&Nfp> = nfps.iter().collect();
185            if let Some((x, y)) = find_bottom_left_placement(&ifp_shrunk, &nfp_refs, sample_step) {
186                // Clamp position to keep geometry within boundary
187                let geom_aabb = geom.aabb_at_rotation(rotation_angle);
188                let boundary_aabb = self.boundary.aabb();
189
190                if let Some((clamped_x, clamped_y)) =
191                    clamp_placement_to_boundary(x, y, geom_aabb, boundary_aabb)
192                {
193                    // Only verify overlap if clamping changed the position
194                    // The original NFP-found position is already collision-free by definition
195                    let was_clamped = (clamped_x - x).abs() > 1e-6 || (clamped_y - y).abs() > 1e-6;
196                    if was_clamped {
197                        // Verify no actual polygon overlap using SAT
198                        if !verify_no_overlap(
199                            geom,
200                            (clamped_x, clamped_y),
201                            rotation_angle,
202                            &placed_geometries,
203                        ) {
204                            continue; // Skip - clamped position would cause overlap
205                        }
206                    }
207
208                    let placement = Placement::new_2d(
209                        geom.id().clone(),
210                        info.instance_num,
211                        clamped_x,
212                        clamped_y,
213                        rotation_angle,
214                    );
215
216                    placements.push(placement);
217                    placed_geometries.push(PlacedGeometry::new(
218                        geom.clone(),
219                        (clamped_x, clamped_y),
220                        rotation_angle,
221                    ));
222                    total_placed_area += geom.measure();
223                    placed_count += 1;
224                }
225            }
226        }
227
228        let utilization = total_placed_area / self.boundary.measure();
229        (placements, utilization, placed_count)
230    }
231
232    /// Gets the boundary polygon with margin applied.
233    fn get_boundary_polygon_with_margin(&self, margin: f64) -> Vec<(f64, f64)> {
234        let (b_min, b_max) = self.boundary.aabb();
235        vec![
236            (b_min[0] + margin, b_min[1] + margin),
237            (b_max[0] - margin, b_min[1] + margin),
238            (b_max[0] - margin, b_max[1] - margin),
239            (b_min[0] + margin, b_max[1] - margin),
240        ]
241    }
242
243    /// Computes an adaptive sample step based on geometry sizes.
244    fn compute_sample_step(&self) -> f64 {
245        if self.geometries.is_empty() {
246            return 1.0;
247        }
248
249        let mut min_dim = f64::INFINITY;
250        for geom in &self.geometries {
251            let (g_min, g_max) = geom.aabb();
252            let width = g_max[0] - g_min[0];
253            let height = g_max[1] - g_min[1];
254            min_dim = min_dim.min(width).min(height);
255        }
256
257        (min_dim / 4.0).clamp(0.5, 10.0)
258    }
259
260    /// Expands an NFP by the given spacing amount.
261    fn expand_nfp(&self, nfp: &Nfp, spacing: f64) -> Nfp {
262        expand_nfp(nfp, spacing)
263    }
264
265    /// Shrinks an IFP by the given spacing amount.
266    fn shrink_ifp(&self, ifp: &Nfp, spacing: f64) -> Nfp {
267        shrink_ifp(ifp, spacing)
268    }
269}
270
271impl BrkgaProblem for BrkgaNestingProblem {
272    fn num_keys(&self) -> usize {
273        // N keys for order + N keys for rotations
274        self.instances.len() * 2
275    }
276
277    fn evaluate(&self, chromosome: &mut RandomKeyChromosome) {
278        let (_, utilization, placed_count) = self.decode(chromosome);
279        let fitness = nesting_fitness(placed_count, self.instances.len(), utilization);
280        chromosome.set_fitness(fitness);
281    }
282
283    fn on_generation(
284        &self,
285        generation: u32,
286        best: &RandomKeyChromosome,
287        _population: &[RandomKeyChromosome],
288    ) {
289        log::debug!(
290            "BRKGA Generation {}: fitness={:.4}",
291            generation,
292            best.fitness()
293        );
294    }
295}
296
297/// Runs BRKGA-based nesting optimization.
298pub fn run_brkga_nesting(
299    geometries: &[Geometry2D],
300    boundary: &Boundary2D,
301    config: &Config,
302    brkga_config: BrkgaConfig,
303    cancelled: Arc<AtomicBool>,
304) -> SolveResult<f64> {
305    let problem = BrkgaNestingProblem::new(
306        geometries.to_vec(),
307        boundary.clone(),
308        config.clone(),
309        cancelled.clone(),
310    );
311
312    let runner = BrkgaRunner::with_cancellation(brkga_config, problem, cancelled.clone());
313
314    // Seed the RNG for reproducibility when `config.seed` is set; otherwise use
315    // system entropy (non-deterministic).
316    let brkga_result = match config.seed {
317        Some(seed) => {
318            use rand::SeedableRng;
319            runner.run_with_rng(&mut rand::rngs::StdRng::seed_from_u64(seed))
320        }
321        None => runner.run(),
322    };
323
324    // Decode the best chromosome to get final placements
325    let problem = BrkgaNestingProblem::new(
326        geometries.to_vec(),
327        boundary.clone(),
328        config.clone(),
329        Arc::new(AtomicBool::new(false)),
330    );
331
332    let (placements, utilization, _placed_count) = problem.decode(&brkga_result.best);
333
334    // Build unplaced list
335    let mut unplaced = Vec::new();
336    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
337    for p in &placements {
338        placed_ids.insert(p.geometry_id.clone());
339    }
340    for geom in geometries {
341        if !placed_ids.contains(geom.id()) {
342            unplaced.push(geom.id().clone());
343        }
344    }
345
346    let mut result = SolveResult::new();
347    result.placements = placements;
348    result.unplaced = unplaced;
349    result.boundaries_used = 1;
350    result.utilization = utilization;
351    result.computation_time_ms = brkga_result.elapsed.as_millis() as u64;
352    result.generations = Some(brkga_result.generations);
353    result.best_fitness = Some(brkga_result.best.fitness());
354    result.fitness_history = Some(brkga_result.history);
355    result.strategy = Some("BRKGA".to_string());
356    result.cancelled = cancelled.load(Ordering::Relaxed);
357    result.target_reached = brkga_result.target_reached;
358
359    result
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_brkga_nesting_basic() {
368        let geometries = vec![
369            Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
370            Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
371        ];
372
373        let boundary = Boundary2D::rectangle(100.0, 50.0);
374        let config = Config::default();
375        let brkga_config = BrkgaConfig::default()
376            .with_population_size(30)
377            .with_max_generations(20);
378
379        let result = run_brkga_nesting(
380            &geometries,
381            &boundary,
382            &config,
383            brkga_config,
384            Arc::new(AtomicBool::new(false)),
385        );
386
387        assert!(result.utilization > 0.0);
388        assert!(!result.placements.is_empty());
389        assert_eq!(result.strategy, Some("BRKGA".to_string()));
390    }
391
392    #[test]
393    fn test_brkga_nesting_all_placed() {
394        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
395
396        let boundary = Boundary2D::rectangle(100.0, 100.0);
397        let config = Config::default();
398        let brkga_config = BrkgaConfig::default()
399            .with_population_size(30)
400            .with_max_generations(30);
401
402        let result = run_brkga_nesting(
403            &geometries,
404            &boundary,
405            &config,
406            brkga_config,
407            Arc::new(AtomicBool::new(false)),
408        );
409
410        // All 4 pieces should fit easily
411        assert_eq!(result.placements.len(), 4);
412        assert!(result.unplaced.is_empty());
413    }
414
415    #[test]
416    fn test_brkga_nesting_with_rotation() {
417        let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
418            .with_quantity(3)
419            .with_rotations(vec![0.0, 90.0])];
420
421        let boundary = Boundary2D::rectangle(50.0, 50.0);
422        let config = Config::default();
423        let brkga_config = BrkgaConfig::default()
424            .with_population_size(30)
425            .with_max_generations(30);
426
427        let result = run_brkga_nesting(
428            &geometries,
429            &boundary,
430            &config,
431            brkga_config,
432            Arc::new(AtomicBool::new(false)),
433        );
434
435        assert!(result.utilization > 0.0);
436        assert!(!result.placements.is_empty());
437    }
438
439    #[test]
440    fn test_brkga_problem_decode() {
441        use rand::SeedableRng;
442
443        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2)];
444
445        let boundary = Boundary2D::rectangle(100.0, 50.0);
446        let config = Config::default();
447        let cancelled = Arc::new(AtomicBool::new(false));
448
449        let problem = BrkgaNestingProblem::new(geometries, boundary, config, cancelled);
450
451        assert_eq!(problem.num_instances(), 2);
452        // 2 instances * 2 (order + rotation) = 4 keys
453        assert_eq!(problem.num_keys(), 4);
454
455        // Create a chromosome with fixed seed for deterministic test
456        let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
457        let chromosome = RandomKeyChromosome::random(problem.num_keys(), &mut rng);
458        let (placements, utilization, placed_count) = problem.decode(&chromosome);
459
460        // Decoding should produce valid output (may or may not place items depending on random keys)
461        assert_eq!(placements.len(), placed_count);
462        if placed_count > 0 {
463            assert!(utilization > 0.0);
464        }
465    }
466}