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