u_nesting_d3/
ga_packing.rs

1//! Genetic Algorithm based 3D bin packing optimization.
2//!
3//! This module provides GA-based optimization for 3D bin packing problems,
4//! using permutation + orientation chromosome representation and layer-based decoding.
5
6use crate::boundary::Boundary3D;
7use crate::geometry::Geometry3D;
8use rand::prelude::*;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11use u_nesting_core::ga::{GaConfig, GaProblem, GaRunner, Individual};
12use u_nesting_core::geometry::{Boundary, Geometry};
13use u_nesting_core::solver::Config;
14use u_nesting_core::{Placement, SolveResult};
15
16/// Packing chromosome representing placement order and orientations.
17#[derive(Debug, Clone)]
18pub struct PackingChromosome {
19    /// Permutation of instance indices (placement order).
20    pub order: Vec<usize>,
21    /// Orientation index for each instance (0-5 for boxes with Any constraint).
22    pub orientations: Vec<usize>,
23    /// Cached fitness value.
24    fitness: f64,
25    /// Number of placed pieces.
26    placed_count: usize,
27    /// Total instances count.
28    total_count: usize,
29}
30
31impl PackingChromosome {
32    /// Creates a new chromosome for the given number of instances.
33    pub fn new(num_instances: usize) -> Self {
34        Self {
35            order: (0..num_instances).collect(),
36            orientations: vec![0; num_instances],
37            fitness: f64::NEG_INFINITY,
38            placed_count: 0,
39            total_count: num_instances,
40        }
41    }
42
43    /// Creates a random chromosome with given orientation options per instance.
44    pub fn random_with_options<R: Rng>(
45        num_instances: usize,
46        orientation_counts: &[usize],
47        rng: &mut R,
48    ) -> Self {
49        let mut order: Vec<usize> = (0..num_instances).collect();
50        order.shuffle(rng);
51
52        let orientations: Vec<usize> = orientation_counts
53            .iter()
54            .map(|&count| rng.gen_range(0..count.max(1)))
55            .collect();
56
57        Self {
58            order,
59            orientations,
60            fitness: f64::NEG_INFINITY,
61            placed_count: 0,
62            total_count: num_instances,
63        }
64    }
65
66    /// Sets the fitness value.
67    pub fn set_fitness(&mut self, fitness: f64, placed_count: usize) {
68        self.fitness = fitness;
69        self.placed_count = placed_count;
70    }
71
72    /// Order crossover (OX1) for permutation genes.
73    pub fn order_crossover<R: Rng>(
74        &self,
75        other: &Self,
76        orientation_counts: &[usize],
77        rng: &mut R,
78    ) -> Self {
79        let n = self.order.len();
80        if n < 2 {
81            return self.clone();
82        }
83
84        // Select two crossover points
85        let (mut p1, mut p2) = (rng.gen_range(0..n), rng.gen_range(0..n));
86        if p1 > p2 {
87            std::mem::swap(&mut p1, &mut p2);
88        }
89
90        // Copy segment from parent1
91        let mut child_order = vec![usize::MAX; n];
92        let mut used = vec![false; n];
93
94        for i in p1..=p2 {
95            child_order[i] = self.order[i];
96            used[self.order[i]] = true;
97        }
98
99        // Fill remaining from parent2
100        let mut j = (p2 + 1) % n;
101        for i in 0..n {
102            let idx = (p2 + 1 + i) % n;
103            if child_order[idx] == usize::MAX {
104                while used[other.order[j]] {
105                    j = (j + 1) % n;
106                }
107                child_order[idx] = other.order[j];
108                used[other.order[j]] = true;
109                j = (j + 1) % n;
110            }
111        }
112
113        // Crossover orientations (uniform with bounds checking)
114        let orientations: Vec<usize> = self
115            .orientations
116            .iter()
117            .zip(&other.orientations)
118            .enumerate()
119            .map(|(i, (a, b))| {
120                let max_orient = orientation_counts.get(i).copied().unwrap_or(1);
121                let chosen = if rng.gen() { *a } else { *b };
122                chosen % max_orient.max(1)
123            })
124            .collect();
125
126        Self {
127            order: child_order,
128            orientations,
129            fitness: f64::NEG_INFINITY,
130            placed_count: 0,
131            total_count: self.total_count,
132        }
133    }
134
135    /// Swap mutation for order genes.
136    pub fn swap_mutate<R: Rng>(&mut self, rng: &mut R) {
137        if self.order.len() < 2 {
138            return;
139        }
140
141        let i = rng.gen_range(0..self.order.len());
142        let j = rng.gen_range(0..self.order.len());
143        self.order.swap(i, j);
144        self.fitness = f64::NEG_INFINITY;
145    }
146
147    /// Orientation mutation.
148    pub fn orientation_mutate<R: Rng>(&mut self, orientation_counts: &[usize], rng: &mut R) {
149        if self.orientations.is_empty() {
150            return;
151        }
152
153        let idx = rng.gen_range(0..self.orientations.len());
154        let max_orient = orientation_counts.get(idx).copied().unwrap_or(1);
155        if max_orient > 1 {
156            self.orientations[idx] = rng.gen_range(0..max_orient);
157            self.fitness = f64::NEG_INFINITY;
158        }
159    }
160
161    /// Inversion mutation (reverses a segment of the order).
162    pub fn inversion_mutate<R: Rng>(&mut self, rng: &mut R) {
163        let n = self.order.len();
164        if n < 2 {
165            return;
166        }
167
168        let (mut p1, mut p2) = (rng.gen_range(0..n), rng.gen_range(0..n));
169        if p1 > p2 {
170            std::mem::swap(&mut p1, &mut p2);
171        }
172
173        self.order[p1..=p2].reverse();
174        self.fitness = f64::NEG_INFINITY;
175    }
176}
177
178impl Individual for PackingChromosome {
179    type Fitness = f64;
180
181    fn fitness(&self) -> f64 {
182        self.fitness
183    }
184
185    fn random<R: Rng>(_rng: &mut R) -> Self {
186        // Default: empty, will be overridden by problem's initialize_population
187        Self::new(0)
188    }
189
190    fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
191        // Default crossover without orientation bounds - should use problem's crossover
192        let fake_counts = vec![6; self.orientations.len()];
193        self.order_crossover(other, &fake_counts, rng)
194    }
195
196    fn mutate<R: Rng>(&mut self, rng: &mut R) {
197        // 50% swap, 30% inversion, 20% orientation
198        let r: f64 = rng.gen();
199        if r < 0.5 {
200            self.swap_mutate(rng);
201        } else if r < 0.8 {
202            self.inversion_mutate(rng);
203        } else {
204            // Orientation mutation with max 6 options
205            let fake_counts = vec![6; self.orientations.len()];
206            self.orientation_mutate(&fake_counts, rng);
207        }
208    }
209}
210
211/// Instance information for decoding.
212#[derive(Debug, Clone)]
213struct InstanceInfo {
214    /// Index into the geometries array.
215    geometry_idx: usize,
216    /// Instance number within this geometry's quantity.
217    instance_num: usize,
218    /// Number of allowed orientations.
219    orientation_count: usize,
220}
221
222/// Problem definition for GA-based 3D bin packing.
223pub struct PackingProblem {
224    /// Input geometries.
225    geometries: Vec<Geometry3D>,
226    /// Boundary container.
227    boundary: Boundary3D,
228    /// Solver configuration.
229    config: Config,
230    /// Instance mapping.
231    instances: Vec<InstanceInfo>,
232    /// Orientation counts per instance.
233    orientation_counts: Vec<usize>,
234    /// Cancellation flag.
235    cancelled: Arc<AtomicBool>,
236}
237
238impl PackingProblem {
239    /// Creates a new packing problem.
240    pub fn new(
241        geometries: Vec<Geometry3D>,
242        boundary: Boundary3D,
243        config: Config,
244        cancelled: Arc<AtomicBool>,
245    ) -> Self {
246        // Build instance mapping
247        let mut instances = Vec::new();
248        let mut orientation_counts = Vec::new();
249
250        for (geom_idx, geom) in geometries.iter().enumerate() {
251            let orient_count = geom.allowed_orientations().len();
252
253            for instance_num in 0..geom.quantity() {
254                instances.push(InstanceInfo {
255                    geometry_idx: geom_idx,
256                    instance_num,
257                    orientation_count: orient_count,
258                });
259                orientation_counts.push(orient_count);
260            }
261        }
262
263        Self {
264            geometries,
265            boundary,
266            config,
267            instances,
268            orientation_counts,
269            cancelled,
270        }
271    }
272
273    /// Returns the total number of instances.
274    pub fn num_instances(&self) -> usize {
275        self.instances.len()
276    }
277
278    /// Returns orientation counts per instance.
279    pub fn orientation_counts(&self) -> &[usize] {
280        &self.orientation_counts
281    }
282
283    /// Decodes a chromosome into placements using layer-based packing.
284    pub fn decode(&self, chromosome: &PackingChromosome) -> (Vec<Placement<f64>>, f64, usize) {
285        let mut placements = Vec::new();
286
287        let margin = self.config.margin;
288        let spacing = self.config.spacing;
289
290        let bound_max_x = self.boundary.width() - margin;
291        let bound_max_y = self.boundary.depth() - margin;
292        let bound_max_z = self.boundary.height() - margin;
293
294        // Track current position in layer-based packing
295        let mut current_x = margin;
296        let mut current_y = margin;
297        let mut current_z = margin;
298        let mut row_depth = 0.0_f64;
299        let mut layer_height = 0.0_f64;
300
301        let mut total_placed_volume = 0.0;
302        let mut total_placed_mass = 0.0;
303        let mut placed_count = 0;
304
305        // Place items in the order specified by chromosome
306        for &instance_idx in &chromosome.order {
307            if self.cancelled.load(Ordering::Relaxed) {
308                break;
309            }
310
311            if instance_idx >= self.instances.len() {
312                continue;
313            }
314
315            let info = &self.instances[instance_idx];
316            let geom = &self.geometries[info.geometry_idx];
317
318            // Get orientation from chromosome
319            let orientation_idx = chromosome
320                .orientations
321                .get(instance_idx)
322                .copied()
323                .unwrap_or(0);
324            let orientation_idx = orientation_idx % info.orientation_count.max(1);
325
326            // Get dimensions for this orientation
327            let dims = geom.dimensions_for_orientation(orientation_idx);
328            let g_width = dims.x;
329            let g_depth = dims.y;
330            let g_height = dims.z;
331
332            // Check mass constraint
333            if let (Some(max_mass), Some(item_mass)) = (self.boundary.max_mass(), geom.mass()) {
334                if total_placed_mass + item_mass > max_mass {
335                    continue;
336                }
337            }
338
339            // Try to fit in current row
340            if current_x + g_width > bound_max_x {
341                // Move to next row
342                current_x = margin;
343                current_y += row_depth + spacing;
344                row_depth = 0.0;
345            }
346
347            // Check if fits in current layer (y direction)
348            if current_y + g_depth > bound_max_y {
349                // Move to next layer
350                current_x = margin;
351                current_y = margin;
352                current_z += layer_height + spacing;
353                row_depth = 0.0;
354                layer_height = 0.0;
355            }
356
357            // Check if fits in container height
358            if current_z + g_height > bound_max_z {
359                continue;
360            }
361
362            // Place the item
363            let placement = Placement::new_3d(
364                geom.id().clone(),
365                info.instance_num,
366                current_x,
367                current_y,
368                current_z,
369                0.0,
370                0.0,
371                0.0, // Orientation is encoded in orientation_idx, not Euler angles
372            );
373
374            placements.push(placement);
375            total_placed_volume += geom.measure();
376            if let Some(mass) = geom.mass() {
377                total_placed_mass += mass;
378            }
379            placed_count += 1;
380
381            // Update position for next item
382            current_x += g_width + spacing;
383            row_depth = row_depth.max(g_depth);
384            layer_height = layer_height.max(g_height);
385        }
386
387        let utilization = total_placed_volume / self.boundary.measure();
388        (placements, utilization, placed_count)
389    }
390}
391
392impl GaProblem for PackingProblem {
393    type Individual = PackingChromosome;
394
395    fn evaluate(&self, individual: &mut Self::Individual) {
396        let (_, utilization, placed_count) = self.decode(individual);
397
398        // Fitness = placement ratio * 100 + utilization * 10
399        let placement_ratio = placed_count as f64 / individual.total_count.max(1) as f64;
400        let fitness = placement_ratio * 100.0 + utilization * 10.0;
401
402        individual.set_fitness(fitness, placed_count);
403    }
404
405    fn initialize_population<R: Rng>(&self, size: usize, rng: &mut R) -> Vec<Self::Individual> {
406        (0..size)
407            .map(|_| {
408                PackingChromosome::random_with_options(
409                    self.num_instances(),
410                    &self.orientation_counts,
411                    rng,
412                )
413            })
414            .collect()
415    }
416
417    fn on_generation(
418        &self,
419        generation: u32,
420        best: &Self::Individual,
421        _population: &[Self::Individual],
422    ) {
423        log::debug!(
424            "GA 3D Packing Gen {}: fitness={:.4}, placed={}/{}",
425            generation,
426            best.fitness(),
427            best.placed_count,
428            best.total_count
429        );
430    }
431}
432
433/// Runs GA-based 3D bin packing optimization.
434pub fn run_ga_packing(
435    geometries: &[Geometry3D],
436    boundary: &Boundary3D,
437    config: &Config,
438    ga_config: GaConfig,
439    cancelled: Arc<AtomicBool>,
440) -> SolveResult<f64> {
441    let problem = PackingProblem::new(
442        geometries.to_vec(),
443        boundary.clone(),
444        config.clone(),
445        cancelled.clone(),
446    );
447
448    let runner = GaRunner::new(ga_config, problem);
449
450    // Connect cancellation
451    let cancel_handle = runner.cancel_handle();
452    let cancelled_clone = cancelled.clone();
453    std::thread::spawn(move || {
454        while !cancelled_clone.load(Ordering::Relaxed) {
455            std::thread::sleep(std::time::Duration::from_millis(100));
456        }
457        cancel_handle.store(true, Ordering::Relaxed);
458    });
459
460    let ga_result = runner.run();
461
462    // Decode the best chromosome
463    let problem = PackingProblem::new(
464        geometries.to_vec(),
465        boundary.clone(),
466        config.clone(),
467        Arc::new(AtomicBool::new(false)),
468    );
469
470    let (placements, utilization, _placed_count) = problem.decode(&ga_result.best);
471
472    // Build unplaced list
473    let mut unplaced = Vec::new();
474    let mut placed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
475    for p in &placements {
476        placed_ids.insert(p.geometry_id.clone());
477    }
478    for geom in geometries {
479        if !placed_ids.contains(geom.id()) {
480            unplaced.push(geom.id().clone());
481        }
482    }
483
484    let mut result = SolveResult::new();
485    result.placements = placements;
486    result.unplaced = unplaced;
487    result.boundaries_used = 1;
488    result.utilization = utilization;
489    result.computation_time_ms = ga_result.elapsed.as_millis() as u64;
490    result.generations = Some(ga_result.generations);
491    result.best_fitness = Some(ga_result.best.fitness());
492    result.fitness_history = Some(ga_result.history);
493    result.strategy = Some("GeneticAlgorithm".to_string());
494    result.cancelled = cancelled.load(Ordering::Relaxed);
495    result.target_reached = ga_result.target_reached;
496
497    result
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    #[test]
505    fn test_packing_chromosome_crossover() {
506        let mut rng = thread_rng();
507        let orientation_counts = vec![6, 6, 6, 6, 6];
508        let parent1 = PackingChromosome::random_with_options(5, &orientation_counts, &mut rng);
509        let parent2 = PackingChromosome::random_with_options(5, &orientation_counts, &mut rng);
510
511        let child = parent1.order_crossover(&parent2, &orientation_counts, &mut rng);
512
513        // Child should be a valid permutation
514        assert_eq!(child.order.len(), 5);
515        let mut sorted = child.order.clone();
516        sorted.sort();
517        assert_eq!(sorted, (0..5).collect::<Vec<_>>());
518
519        // Orientations should be within bounds
520        for (i, &orient) in child.orientations.iter().enumerate() {
521            assert!(orient < orientation_counts[i]);
522        }
523    }
524
525    #[test]
526    fn test_packing_chromosome_mutation() {
527        let mut rng = thread_rng();
528        let orientation_counts = vec![6, 6, 6, 6, 6];
529        let mut chromosome =
530            PackingChromosome::random_with_options(5, &orientation_counts, &mut rng);
531
532        chromosome.swap_mutate(&mut rng);
533
534        // Should still be a valid permutation
535        let mut sorted = chromosome.order.clone();
536        sorted.sort();
537        assert_eq!(sorted, (0..5).collect::<Vec<_>>());
538    }
539
540    #[test]
541    fn test_ga_packing_basic() {
542        let geometries = vec![
543            Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
544            Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
545        ];
546
547        let boundary = Boundary3D::new(100.0, 80.0, 50.0);
548        let config = Config::default();
549        let ga_config = GaConfig::default()
550            .with_population_size(20)
551            .with_max_generations(10);
552
553        let result = run_ga_packing(
554            &geometries,
555            &boundary,
556            &config,
557            ga_config,
558            Arc::new(AtomicBool::new(false)),
559        );
560
561        assert!(result.utilization > 0.0);
562        assert!(!result.placements.is_empty());
563    }
564
565    #[test]
566    fn test_ga_packing_all_placed() {
567        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(4)];
568
569        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
570        let config = Config::default();
571        let ga_config = GaConfig::default()
572            .with_population_size(30)
573            .with_max_generations(20);
574
575        let result = run_ga_packing(
576            &geometries,
577            &boundary,
578            &config,
579            ga_config,
580            Arc::new(AtomicBool::new(false)),
581        );
582
583        // All 4 boxes should fit easily
584        assert_eq!(result.placements.len(), 4);
585        assert!(result.unplaced.is_empty());
586    }
587
588    #[test]
589    fn test_ga_packing_with_orientations() {
590        use crate::geometry::OrientationConstraint;
591        // Long boxes that benefit from rotation
592        let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
593            .with_quantity(3)
594            .with_orientation(OrientationConstraint::Any)];
595
596        let boundary = Boundary3D::new(60.0, 60.0, 60.0);
597        let config = Config::default();
598        let ga_config = GaConfig::default()
599            .with_population_size(30)
600            .with_max_generations(20);
601
602        let result = run_ga_packing(
603            &geometries,
604            &boundary,
605            &config,
606            ga_config,
607            Arc::new(AtomicBool::new(false)),
608        );
609
610        assert!(result.utilization > 0.0);
611        assert!(!result.placements.is_empty());
612    }
613
614    #[test]
615    fn test_packing_problem_decode() {
616        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2)];
617
618        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
619        let config = Config::default();
620        let cancelled = Arc::new(AtomicBool::new(false));
621
622        let problem = PackingProblem::new(geometries, boundary, config, cancelled);
623
624        assert_eq!(problem.num_instances(), 2);
625
626        // Create a chromosome and decode
627        let chromosome = PackingChromosome::new(2);
628        let (placements, utilization, placed_count) = problem.decode(&chromosome);
629
630        assert_eq!(placed_count, 2);
631        assert_eq!(placements.len(), 2);
632        assert!(utilization > 0.0);
633    }
634
635    #[test]
636    fn test_ga_packing_mass_constraint() {
637        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0)
638            .with_quantity(10)
639            .with_mass(100.0)];
640
641        let boundary = Boundary3D::new(100.0, 100.0, 100.0).with_max_mass(350.0);
642        let config = Config::default();
643        let ga_config = GaConfig::default()
644            .with_population_size(20)
645            .with_max_generations(10);
646
647        let result = run_ga_packing(
648            &geometries,
649            &boundary,
650            &config,
651            ga_config,
652            Arc::new(AtomicBool::new(false)),
653        );
654
655        // Should only place 3 boxes due to 350 mass limit
656        assert!(result.placements.len() <= 3);
657    }
658}