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    let brkga_result = runner.run();
315
316    // Decode the best chromosome to get final placements
317    let problem = BrkgaNestingProblem::new(
318        geometries.to_vec(),
319        boundary.clone(),
320        config.clone(),
321        Arc::new(AtomicBool::new(false)),
322    );
323
324    let (placements, utilization, _placed_count) = problem.decode(&brkga_result.best);
325
326    // Build unplaced list
327    let mut unplaced = Vec::new();
328    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
329    for p in &placements {
330        placed_ids.insert(p.geometry_id.clone());
331    }
332    for geom in geometries {
333        if !placed_ids.contains(geom.id()) {
334            unplaced.push(geom.id().clone());
335        }
336    }
337
338    let mut result = SolveResult::new();
339    result.placements = placements;
340    result.unplaced = unplaced;
341    result.boundaries_used = 1;
342    result.utilization = utilization;
343    result.computation_time_ms = brkga_result.elapsed.as_millis() as u64;
344    result.generations = Some(brkga_result.generations);
345    result.best_fitness = Some(brkga_result.best.fitness());
346    result.fitness_history = Some(brkga_result.history);
347    result.strategy = Some("BRKGA".to_string());
348    result.cancelled = cancelled.load(Ordering::Relaxed);
349    result.target_reached = brkga_result.target_reached;
350
351    result
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    #[test]
359    fn test_brkga_nesting_basic() {
360        let geometries = vec![
361            Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2),
362            Geometry2D::rectangle("R2", 15.0, 15.0).with_quantity(2),
363        ];
364
365        let boundary = Boundary2D::rectangle(100.0, 50.0);
366        let config = Config::default();
367        let brkga_config = BrkgaConfig::default()
368            .with_population_size(30)
369            .with_max_generations(20);
370
371        let result = run_brkga_nesting(
372            &geometries,
373            &boundary,
374            &config,
375            brkga_config,
376            Arc::new(AtomicBool::new(false)),
377        );
378
379        assert!(result.utilization > 0.0);
380        assert!(!result.placements.is_empty());
381        assert_eq!(result.strategy, Some("BRKGA".to_string()));
382    }
383
384    #[test]
385    fn test_brkga_nesting_all_placed() {
386        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 20.0).with_quantity(4)];
387
388        let boundary = Boundary2D::rectangle(100.0, 100.0);
389        let config = Config::default();
390        let brkga_config = BrkgaConfig::default()
391            .with_population_size(30)
392            .with_max_generations(30);
393
394        let result = run_brkga_nesting(
395            &geometries,
396            &boundary,
397            &config,
398            brkga_config,
399            Arc::new(AtomicBool::new(false)),
400        );
401
402        // All 4 pieces should fit easily
403        assert_eq!(result.placements.len(), 4);
404        assert!(result.unplaced.is_empty());
405    }
406
407    #[test]
408    fn test_brkga_nesting_with_rotation() {
409        let geometries = vec![Geometry2D::rectangle("R1", 30.0, 10.0)
410            .with_quantity(3)
411            .with_rotations(vec![0.0, 90.0])];
412
413        let boundary = Boundary2D::rectangle(50.0, 50.0);
414        let config = Config::default();
415        let brkga_config = BrkgaConfig::default()
416            .with_population_size(30)
417            .with_max_generations(30);
418
419        let result = run_brkga_nesting(
420            &geometries,
421            &boundary,
422            &config,
423            brkga_config,
424            Arc::new(AtomicBool::new(false)),
425        );
426
427        assert!(result.utilization > 0.0);
428        assert!(!result.placements.is_empty());
429    }
430
431    #[test]
432    fn test_brkga_problem_decode() {
433        use rand::SeedableRng;
434
435        let geometries = vec![Geometry2D::rectangle("R1", 20.0, 10.0).with_quantity(2)];
436
437        let boundary = Boundary2D::rectangle(100.0, 50.0);
438        let config = Config::default();
439        let cancelled = Arc::new(AtomicBool::new(false));
440
441        let problem = BrkgaNestingProblem::new(geometries, boundary, config, cancelled);
442
443        assert_eq!(problem.num_instances(), 2);
444        // 2 instances * 2 (order + rotation) = 4 keys
445        assert_eq!(problem.num_keys(), 4);
446
447        // Create a chromosome with fixed seed for deterministic test
448        let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
449        let chromosome = RandomKeyChromosome::random(problem.num_keys(), &mut rng);
450        let (placements, utilization, placed_count) = problem.decode(&chromosome);
451
452        // Decoding should produce valid output (may or may not place items depending on random keys)
453        assert_eq!(placements.len(), placed_count);
454        if placed_count > 0 {
455            assert!(utilization > 0.0);
456        }
457    }
458}