Skip to main content

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 crate::packing_utils::{
9    build_instances, build_unplaced_list, layer_place_items, packing_fitness, InstanceInfo,
10    PlacementItem,
11};
12use rand::prelude::*;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::Arc;
15use u_nesting_core::ga::{GaConfig, GaProblem, GaRunner, Individual};
16use u_nesting_core::solver::Config;
17use u_nesting_core::SolveResult;
18
19/// Packing chromosome representing placement order and orientations.
20#[derive(Debug, Clone)]
21pub struct PackingChromosome {
22    /// Permutation of instance indices (placement order).
23    pub order: Vec<usize>,
24    /// Orientation index for each instance (0-5 for boxes with Any constraint).
25    pub orientations: Vec<usize>,
26    /// Cached fitness value.
27    fitness: f64,
28    /// Number of placed pieces.
29    placed_count: usize,
30    /// Total instances count.
31    total_count: usize,
32}
33
34impl PackingChromosome {
35    /// Creates a new chromosome for the given number of instances.
36    pub fn new(num_instances: usize) -> Self {
37        Self {
38            order: (0..num_instances).collect(),
39            orientations: vec![0; num_instances],
40            fitness: f64::NEG_INFINITY,
41            placed_count: 0,
42            total_count: num_instances,
43        }
44    }
45
46    /// Creates a random chromosome with given orientation options per instance.
47    pub fn random_with_options<R: Rng>(
48        num_instances: usize,
49        orientation_counts: &[usize],
50        rng: &mut R,
51    ) -> Self {
52        let mut order: Vec<usize> = (0..num_instances).collect();
53        order.shuffle(rng);
54
55        let orientations: Vec<usize> = orientation_counts
56            .iter()
57            .map(|&count| rng.random_range(0..count.max(1)))
58            .collect();
59
60        Self {
61            order,
62            orientations,
63            fitness: f64::NEG_INFINITY,
64            placed_count: 0,
65            total_count: num_instances,
66        }
67    }
68
69    /// Sets the fitness value.
70    pub fn set_fitness(&mut self, fitness: f64, placed_count: usize) {
71        self.fitness = fitness;
72        self.placed_count = placed_count;
73    }
74
75    /// Order crossover (OX1) for permutation genes.
76    pub fn order_crossover<R: Rng>(
77        &self,
78        other: &Self,
79        orientation_counts: &[usize],
80        rng: &mut R,
81    ) -> Self {
82        let n = self.order.len();
83        if n < 2 {
84            return self.clone();
85        }
86
87        // Select two crossover points
88        let (mut p1, mut p2) = (rng.random_range(0..n), rng.random_range(0..n));
89        if p1 > p2 {
90            std::mem::swap(&mut p1, &mut p2);
91        }
92
93        // Copy segment from parent1
94        let mut child_order = vec![usize::MAX; n];
95        let mut used = vec![false; n];
96
97        for i in p1..=p2 {
98            child_order[i] = self.order[i];
99            used[self.order[i]] = true;
100        }
101
102        // Fill remaining from parent2
103        let mut j = (p2 + 1) % n;
104        for i in 0..n {
105            let idx = (p2 + 1 + i) % n;
106            if child_order[idx] == usize::MAX {
107                while used[other.order[j]] {
108                    j = (j + 1) % n;
109                }
110                child_order[idx] = other.order[j];
111                used[other.order[j]] = true;
112                j = (j + 1) % n;
113            }
114        }
115
116        // Crossover orientations (uniform with bounds checking)
117        let orientations: Vec<usize> = self
118            .orientations
119            .iter()
120            .zip(&other.orientations)
121            .enumerate()
122            .map(|(i, (a, b))| {
123                let max_orient = orientation_counts.get(i).copied().unwrap_or(1);
124                let chosen = if rng.random() { *a } else { *b };
125                chosen % max_orient.max(1)
126            })
127            .collect();
128
129        Self {
130            order: child_order,
131            orientations,
132            fitness: f64::NEG_INFINITY,
133            placed_count: 0,
134            total_count: self.total_count,
135        }
136    }
137
138    /// Swap mutation for order genes.
139    pub fn swap_mutate<R: Rng>(&mut self, rng: &mut R) {
140        if self.order.len() < 2 {
141            return;
142        }
143
144        let i = rng.random_range(0..self.order.len());
145        let j = rng.random_range(0..self.order.len());
146        self.order.swap(i, j);
147        self.fitness = f64::NEG_INFINITY;
148    }
149
150    /// Orientation mutation.
151    pub fn orientation_mutate<R: Rng>(&mut self, orientation_counts: &[usize], rng: &mut R) {
152        if self.orientations.is_empty() {
153            return;
154        }
155
156        let idx = rng.random_range(0..self.orientations.len());
157        let max_orient = orientation_counts.get(idx).copied().unwrap_or(1);
158        if max_orient > 1 {
159            self.orientations[idx] = rng.random_range(0..max_orient);
160            self.fitness = f64::NEG_INFINITY;
161        }
162    }
163
164    /// Inversion mutation (reverses a segment of the order).
165    pub fn inversion_mutate<R: Rng>(&mut self, rng: &mut R) {
166        let n = self.order.len();
167        if n < 2 {
168            return;
169        }
170
171        let (mut p1, mut p2) = (rng.random_range(0..n), rng.random_range(0..n));
172        if p1 > p2 {
173            std::mem::swap(&mut p1, &mut p2);
174        }
175
176        self.order[p1..=p2].reverse();
177        self.fitness = f64::NEG_INFINITY;
178    }
179}
180
181impl Individual for PackingChromosome {
182    type Fitness = f64;
183
184    fn fitness(&self) -> f64 {
185        self.fitness
186    }
187
188    fn random<R: Rng>(_rng: &mut R) -> Self {
189        // Default: empty, will be overridden by problem's initialize_population
190        Self::new(0)
191    }
192
193    fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
194        // Default crossover without orientation bounds - should use problem's crossover
195        let fake_counts = vec![6; self.orientations.len()];
196        self.order_crossover(other, &fake_counts, rng)
197    }
198
199    fn mutate<R: Rng>(&mut self, rng: &mut R) {
200        // 50% swap, 30% inversion, 20% orientation
201        let r: f64 = rng.random();
202        if r < 0.5 {
203            self.swap_mutate(rng);
204        } else if r < 0.8 {
205            self.inversion_mutate(rng);
206        } else {
207            // Orientation mutation with max 6 options
208            let fake_counts = vec![6; self.orientations.len()];
209            self.orientation_mutate(&fake_counts, rng);
210        }
211    }
212}
213
214/// Problem definition for GA-based 3D bin packing.
215pub struct PackingProblem {
216    /// Input geometries.
217    geometries: Vec<Geometry3D>,
218    /// Boundary container.
219    boundary: Boundary3D,
220    /// Solver configuration.
221    config: Config,
222    /// Instance mapping.
223    instances: Vec<InstanceInfo>,
224    /// Orientation counts per instance.
225    orientation_counts: Vec<usize>,
226    /// Cancellation flag.
227    cancelled: Arc<AtomicBool>,
228}
229
230impl PackingProblem {
231    /// Creates a new packing problem.
232    pub fn new(
233        geometries: Vec<Geometry3D>,
234        boundary: Boundary3D,
235        config: Config,
236        cancelled: Arc<AtomicBool>,
237    ) -> Self {
238        let instances = build_instances(&geometries);
239        let orientation_counts: Vec<usize> =
240            instances.iter().map(|i| i.orientation_count).collect();
241
242        Self {
243            geometries,
244            boundary,
245            config,
246            instances,
247            orientation_counts,
248            cancelled,
249        }
250    }
251
252    /// Returns the total number of instances.
253    pub fn num_instances(&self) -> usize {
254        self.instances.len()
255    }
256
257    /// Returns orientation counts per instance.
258    pub fn orientation_counts(&self) -> &[usize] {
259        &self.orientation_counts
260    }
261
262    /// Decodes a chromosome into placements using layer-based packing.
263    pub fn decode(
264        &self,
265        chromosome: &PackingChromosome,
266    ) -> (Vec<u_nesting_core::Placement<f64>>, f64, usize) {
267        let items: Vec<PlacementItem> = chromosome
268            .order
269            .iter()
270            .map(|&idx| PlacementItem {
271                instance_idx: idx,
272                orientation_idx: chromosome.orientations.get(idx).copied().unwrap_or(0),
273            })
274            .collect();
275
276        let result = layer_place_items(
277            &items,
278            &self.instances,
279            &self.geometries,
280            &self.boundary,
281            &self.config,
282            &self.cancelled,
283        );
284        (result.placements, result.utilization, result.placed_count)
285    }
286}
287
288impl GaProblem for PackingProblem {
289    type Individual = PackingChromosome;
290
291    fn evaluate(&self, individual: &mut Self::Individual) {
292        let (_, utilization, placed_count) = self.decode(individual);
293        let fitness = packing_fitness(placed_count, individual.total_count, utilization);
294        individual.set_fitness(fitness, placed_count);
295    }
296
297    fn initialize_population<R: Rng>(&self, size: usize, rng: &mut R) -> Vec<Self::Individual> {
298        (0..size)
299            .map(|_| {
300                PackingChromosome::random_with_options(
301                    self.num_instances(),
302                    &self.orientation_counts,
303                    rng,
304                )
305            })
306            .collect()
307    }
308
309    fn on_generation(
310        &self,
311        generation: u32,
312        best: &Self::Individual,
313        _population: &[Self::Individual],
314    ) {
315        log::debug!(
316            "GA 3D Packing Gen {}: fitness={:.4}, placed={}/{}",
317            generation,
318            best.fitness(),
319            best.placed_count,
320            best.total_count
321        );
322    }
323}
324
325/// Runs GA-based 3D bin packing optimization.
326pub fn run_ga_packing(
327    geometries: &[Geometry3D],
328    boundary: &Boundary3D,
329    config: &Config,
330    ga_config: GaConfig,
331    cancelled: Arc<AtomicBool>,
332) -> SolveResult<f64> {
333    let problem = PackingProblem::new(
334        geometries.to_vec(),
335        boundary.clone(),
336        config.clone(),
337        cancelled.clone(),
338    );
339
340    let runner = GaRunner::new(ga_config, problem);
341
342    // Connect cancellation (thread-based polling, not available on WASM)
343    #[cfg(not(target_arch = "wasm32"))]
344    {
345        let cancel_handle = runner.cancel_handle();
346        let cancelled_clone = cancelled.clone();
347        std::thread::spawn(move || {
348            while !cancelled_clone.load(Ordering::Relaxed) {
349                std::thread::sleep(std::time::Duration::from_millis(100));
350            }
351            cancel_handle.store(true, Ordering::Relaxed);
352        });
353    }
354
355    let ga_result = runner.run();
356
357    // Decode the best chromosome
358    let problem = PackingProblem::new(
359        geometries.to_vec(),
360        boundary.clone(),
361        config.clone(),
362        Arc::new(AtomicBool::new(false)),
363    );
364
365    let (placements, utilization, _placed_count) = problem.decode(&ga_result.best);
366    let unplaced = build_unplaced_list(&placements, geometries);
367
368    let mut result = SolveResult::new();
369    result.placements = placements;
370    result.unplaced = unplaced;
371    result.boundaries_used = 1;
372    result.utilization = utilization;
373    result.computation_time_ms = ga_result.elapsed.as_millis() as u64;
374    result.generations = Some(ga_result.generations);
375    result.best_fitness = Some(ga_result.best.fitness());
376    result.fitness_history = Some(ga_result.history);
377    result.strategy = Some("GeneticAlgorithm".to_string());
378    result.cancelled = cancelled.load(Ordering::Relaxed);
379    result.target_reached = ga_result.target_reached;
380
381    result
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn test_packing_chromosome_crossover() {
390        let mut rng = rand::rng();
391        let orientation_counts = vec![6, 6, 6, 6, 6];
392        let parent1 = PackingChromosome::random_with_options(5, &orientation_counts, &mut rng);
393        let parent2 = PackingChromosome::random_with_options(5, &orientation_counts, &mut rng);
394
395        let child = parent1.order_crossover(&parent2, &orientation_counts, &mut rng);
396
397        // Child should be a valid permutation
398        assert_eq!(child.order.len(), 5);
399        let mut sorted = child.order.clone();
400        sorted.sort();
401        assert_eq!(sorted, (0..5).collect::<Vec<_>>());
402
403        // Orientations should be within bounds
404        for (i, &orient) in child.orientations.iter().enumerate() {
405            assert!(orient < orientation_counts[i]);
406        }
407    }
408
409    #[test]
410    fn test_packing_chromosome_mutation() {
411        let mut rng = rand::rng();
412        let orientation_counts = vec![6, 6, 6, 6, 6];
413        let mut chromosome =
414            PackingChromosome::random_with_options(5, &orientation_counts, &mut rng);
415
416        chromosome.swap_mutate(&mut rng);
417
418        // Should still be a valid permutation
419        let mut sorted = chromosome.order.clone();
420        sorted.sort();
421        assert_eq!(sorted, (0..5).collect::<Vec<_>>());
422    }
423
424    #[test]
425    fn test_ga_packing_basic() {
426        let geometries = vec![
427            Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2),
428            Geometry3D::new("B2", 15.0, 15.0, 15.0).with_quantity(2),
429        ];
430
431        let boundary = Boundary3D::new(100.0, 80.0, 50.0);
432        let config = Config::default();
433        let ga_config = GaConfig::default()
434            .with_population_size(20)
435            .with_max_generations(10);
436
437        let result = run_ga_packing(
438            &geometries,
439            &boundary,
440            &config,
441            ga_config,
442            Arc::new(AtomicBool::new(false)),
443        );
444
445        assert!(result.utilization > 0.0);
446        assert!(!result.placements.is_empty());
447    }
448
449    #[test]
450    fn test_ga_packing_all_placed() {
451        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(4)];
452
453        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
454        let config = Config::default();
455        let ga_config = GaConfig::default()
456            .with_population_size(30)
457            .with_max_generations(20);
458
459        let result = run_ga_packing(
460            &geometries,
461            &boundary,
462            &config,
463            ga_config,
464            Arc::new(AtomicBool::new(false)),
465        );
466
467        // All 4 boxes should fit easily
468        assert_eq!(result.placements.len(), 4);
469        assert!(result.unplaced.is_empty());
470    }
471
472    #[test]
473    fn test_ga_packing_with_orientations() {
474        use crate::geometry::OrientationConstraint;
475        // Long boxes that benefit from rotation
476        let geometries = vec![Geometry3D::new("B1", 50.0, 10.0, 10.0)
477            .with_quantity(3)
478            .with_orientation(OrientationConstraint::Any)];
479
480        let boundary = Boundary3D::new(60.0, 60.0, 60.0);
481        let config = Config::default();
482        let ga_config = GaConfig::default()
483            .with_population_size(30)
484            .with_max_generations(20);
485
486        let result = run_ga_packing(
487            &geometries,
488            &boundary,
489            &config,
490            ga_config,
491            Arc::new(AtomicBool::new(false)),
492        );
493
494        assert!(result.utilization > 0.0);
495        assert!(!result.placements.is_empty());
496    }
497
498    #[test]
499    fn test_packing_problem_decode() {
500        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0).with_quantity(2)];
501
502        let boundary = Boundary3D::new(100.0, 100.0, 100.0);
503        let config = Config::default();
504        let cancelled = Arc::new(AtomicBool::new(false));
505
506        let problem = PackingProblem::new(geometries, boundary, config, cancelled);
507
508        assert_eq!(problem.num_instances(), 2);
509
510        // Create a chromosome and decode
511        let chromosome = PackingChromosome::new(2);
512        let (placements, utilization, placed_count) = problem.decode(&chromosome);
513
514        assert_eq!(placed_count, 2);
515        assert_eq!(placements.len(), 2);
516        assert!(utilization > 0.0);
517    }
518
519    #[test]
520    fn test_ga_packing_mass_constraint() {
521        let geometries = vec![Geometry3D::new("B1", 20.0, 20.0, 20.0)
522            .with_quantity(10)
523            .with_mass(100.0)];
524
525        let boundary = Boundary3D::new(100.0, 100.0, 100.0).with_max_mass(350.0);
526        let config = Config::default();
527        let ga_config = GaConfig::default()
528            .with_population_size(20)
529            .with_max_generations(10);
530
531        let result = run_ga_packing(
532            &geometries,
533            &boundary,
534            &config,
535            ga_config,
536            Arc::new(AtomicBool::new(false)),
537        );
538
539        // Should only place 3 boxes due to 350 mass limit
540        assert!(result.placements.len() <= 3);
541    }
542}