1#![deny(missing_docs)]
6
7mod basic;
8mod genetic;
9
10#[cfg(test)]
11mod tests;
12
13use basic::BasicBin;
14use fnv::FnvHashSet;
15use genetic::population::Population;
16use genetic::unit::Unit;
17use rand::prelude::*;
18use rand::seq::SliceRandom;
19use std::borrow::Borrow;
20use std::cmp;
21use std::hash::{Hash, Hasher};
22
23#[cfg(feature = "serialize")]
24use serde::{Deserialize, Serialize};
25
26#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
28#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
29#[derive(Clone, Debug)]
30pub struct CutPiece {
31 pub quantity: usize,
33
34 pub external_id: Option<usize>,
38
39 pub length: usize,
41}
42
43#[derive(Clone, Debug)]
44pub(crate) struct CutPieceWithId {
45 pub(crate) id: usize,
46 pub(crate) external_id: Option<usize>,
47 pub(crate) length: usize,
48}
49
50impl Hash for CutPieceWithId {
51 fn hash<H: Hasher>(&self, state: &mut H) {
52 self.id.hash(state);
53 }
54}
55impl PartialEq for CutPieceWithId {
56 fn eq(&self, other: &CutPieceWithId) -> bool {
57 self.id == other.id
58 }
59}
60impl Eq for CutPieceWithId {}
61
62#[derive(Clone, Debug)]
63pub(crate) struct UsedCutPiece {
64 pub(crate) id: usize,
65 pub(crate) external_id: Option<usize>,
66 pub(crate) start: usize,
67 pub(crate) end: usize,
68}
69
70impl UsedCutPiece {
71 pub(crate) fn length(&self) -> usize {
72 self.end - self.start
73 }
74}
75
76impl PartialEq for UsedCutPiece {
77 fn eq(&self, other: &UsedCutPiece) -> bool {
78 self.id == other.id
79 }
80}
81impl Eq for UsedCutPiece {}
82
83impl From<&UsedCutPiece> for CutPieceWithId {
84 fn from(used_cut_piece: &UsedCutPiece) -> Self {
85 Self {
86 id: used_cut_piece.id,
87 external_id: used_cut_piece.external_id,
88 length: used_cut_piece.end - used_cut_piece.start,
89 }
90 }
91}
92
93impl From<&UsedCutPiece> for ResultCutPiece {
94 fn from(used_cut_piece: &UsedCutPiece) -> Self {
95 Self {
96 external_id: used_cut_piece.external_id,
97 start: used_cut_piece.start,
98 end: used_cut_piece.end,
99 }
100 }
101}
102
103#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
105#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct ResultCutPiece {
108 pub external_id: Option<usize>,
110
111 pub start: usize,
113
114 pub end: usize,
116}
117
118#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
121#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
122#[derive(Hash, Copy, Clone, Debug, Eq, PartialEq)]
123pub struct StockPiece {
124 pub length: usize,
126
127 pub price: usize,
130
131 pub quantity: Option<usize>,
133}
134
135impl StockPiece {
136 fn dec_quantity(&mut self) {
138 if let Some(ref mut quantity) = self.quantity {
139 *quantity -= 1;
140 }
141 }
142
143 fn inc_quantity(&mut self) {
145 if let Some(ref mut quantity) = self.quantity {
146 *quantity += 1;
147 }
148 }
149}
150
151#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
153#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
154#[derive(Clone, Debug)]
155pub struct ResultStockPiece {
156 pub length: usize,
158
159 pub cut_pieces: Vec<ResultCutPiece>,
161
162 pub price: usize,
164}
165
166trait Bin {
168 fn new(length: usize, blade_width: usize, price: usize) -> Self;
170
171 fn fitness(&self) -> f64;
173
174 fn price(&self) -> usize;
175
176 fn remove_cut_pieces<I>(&mut self, cut_pieces: I) -> usize
178 where
179 I: Iterator,
180 I::Item: Borrow<UsedCutPiece>;
181
182 fn cut_pieces(&self) -> std::slice::Iter<'_, UsedCutPiece>;
184
185 fn insert_cut_piece(&mut self, cut_piece: &CutPieceWithId) -> bool;
187
188 fn matches_stock_piece(&self, stock_piece: &StockPiece) -> bool;
190}
191
192struct OptimizerUnit<'a, B>
193where
194 B: Bin,
195{
196 bins: Vec<B>,
197
198 possible_stock_pieces: &'a [StockPiece],
200
201 available_stock_pieces: Vec<StockPiece>,
203
204 unused_cut_pieces: FnvHashSet<CutPieceWithId>,
206
207 blade_width: usize,
208}
209
210impl<'a, B> Clone for OptimizerUnit<'a, B>
211where
212 B: Bin + Clone,
213{
214 fn clone(&self) -> Self {
215 Self {
216 bins: self.bins.clone(),
217 possible_stock_pieces: self.possible_stock_pieces,
218 available_stock_pieces: self.available_stock_pieces.to_vec(),
219 unused_cut_pieces: self.unused_cut_pieces.clone(),
220 blade_width: self.blade_width,
221 }
222 }
223}
224
225impl<'a, B> OptimizerUnit<'a, B>
226where
227 B: Bin,
228{
229 fn new<R>(
230 possible_stock_pieces: &'a [StockPiece],
231 cut_pieces: &[&CutPieceWithId],
232 blade_width: usize,
233 rng: &mut R,
234 ) -> Result<OptimizerUnit<'a, B>>
235 where
236 R: Rng + ?Sized,
237 {
238 let mut unit = OptimizerUnit {
239 bins: Vec::new(),
240 possible_stock_pieces,
241 available_stock_pieces: possible_stock_pieces.to_vec(),
242 unused_cut_pieces: Default::default(),
243 blade_width,
244 };
245
246 for cut_piece in cut_pieces {
247 if !unit.insert_cut_piece(cut_piece, rng) {
248 unit.unused_cut_pieces.insert((*cut_piece).clone());
249 }
250 }
251
252 Ok(unit)
253 }
254
255 pub(crate) fn generate_initial_units(
256 possible_stock_pieces: &'a [StockPiece],
257 mut cut_pieces: Vec<&CutPieceWithId>,
258 blade_width: usize,
259 random_seed: u64,
260 ) -> Result<Vec<OptimizerUnit<'a, B>>> {
261 let length_set: FnvHashSet<usize> = cut_pieces.iter().map(|p| p.length).collect();
262 let unique_cut_lengths = length_set.len();
263
264 let num_units = {
265 let denom = if cut_pieces.len() > 1 {
266 (cut_pieces.len() as f64).log10()
267 } else {
268 1.0
269 };
270
271 cmp::max(
272 10,
273 (cut_pieces.len() as f64 / denom + ((unique_cut_lengths - 1) * 10) as f64) as usize,
274 )
275 };
276 let mut units = Vec::with_capacity(num_units);
277 let mut rng: StdRng = SeedableRng::seed_from_u64(random_seed);
278
279 cut_pieces.sort_by_key(|p| p.length);
281 units.push(OptimizerUnit::new(
282 possible_stock_pieces,
283 &cut_pieces,
284 blade_width,
285 &mut rng,
286 )?);
287
288 cut_pieces.sort_by_key(|p| cmp::Reverse(p.length));
290 units.push(OptimizerUnit::new(
291 possible_stock_pieces,
292 &cut_pieces,
293 blade_width,
294 &mut rng,
295 )?);
296
297 for _ in 0..num_units - units.len() {
299 cut_pieces.shuffle(&mut rng);
300 units.push(OptimizerUnit::new(
301 possible_stock_pieces,
302 &cut_pieces,
303 blade_width,
304 &mut rng,
305 )?);
306 }
307
308 Ok(units)
309 }
310
311 fn insert_cut_piece<R>(&mut self, cut_piece: &CutPieceWithId, rng: &mut R) -> bool
312 where
313 R: Rng + ?Sized,
314 {
315 for bin in self.bins.iter_mut() {
316 if bin.insert_cut_piece(cut_piece) {
317 return true;
318 }
319 }
320
321 self.add_to_new_bin(cut_piece, rng)
322 }
323
324 fn add_to_new_bin<R>(&mut self, cut_piece: &CutPieceWithId, rng: &mut R) -> bool
325 where
326 R: Rng + ?Sized,
327 {
328 let stock_pieces = self
329 .available_stock_pieces
330 .iter_mut()
331 .filter(|stock_piece| {
332 stock_piece.quantity != Some(0) && cut_piece.length <= stock_piece.length
333 });
334
335 match stock_pieces.choose(rng) {
336 Some(stock_piece) => {
337 stock_piece.dec_quantity();
338
339 let mut bin = B::new(stock_piece.length, self.blade_width, stock_piece.price);
340 if !bin.insert_cut_piece(cut_piece) {
341 return false;
342 }
343 self.bins.push(bin);
344 true
345 }
346 None => false,
347 }
348 }
349
350 fn crossover<R>(&self, other: &OptimizerUnit<'a, B>, rng: &mut R) -> OptimizerUnit<'a, B>
351 where
352 R: Rng + ?Sized,
353 B: Clone,
354 {
355 if self.bins.len() < 2 && other.bins.len() < 2 {
358 return self.clone();
359 }
360
361 let cross_dest = rng.gen_range(0..=self.bins.len());
363 let cross_src_start = rng.gen_range(0..other.bins.len());
364 let cross_src_end = rng.gen_range(cross_src_start + 1..=other.bins.len());
365
366 let mut new_unit = OptimizerUnit {
368 bins: (&self.bins[..cross_dest])
369 .iter()
370 .chain((&other.bins[cross_src_start..cross_src_end]).iter())
371 .chain((&self.bins[cross_dest..]).iter())
372 .cloned()
373 .collect(),
374 possible_stock_pieces: self.possible_stock_pieces,
375 available_stock_pieces: self.possible_stock_pieces.to_vec(),
377 unused_cut_pieces: Default::default(),
379 blade_width: self.blade_width,
380 };
381
382 let mut unused_cut_pieces = self.unused_cut_pieces.clone();
383
384 other.bins[cross_src_start..cross_src_end]
386 .iter()
387 .for_each(|bin| {
388 if let Some(ref mut stock_piece) = new_unit
389 .available_stock_pieces
390 .iter_mut()
391 .find(|sp| bin.matches_stock_piece(sp))
392 {
393 for cut_piece in bin.cut_pieces() {
395 unused_cut_pieces.remove(&cut_piece.into());
396 }
397 stock_piece.dec_quantity();
398 } else {
399 panic!("Attempt to inject invalid bin in crossover operation. This shouldn't happen, and means there is a bug in the code.");
400 }
401 });
402
403 for i in (0..cross_dest)
406 .chain((cross_dest + cross_src_end - cross_src_start)..new_unit.bins.len())
407 .rev()
408 {
409 let bin = &mut new_unit.bins[i];
410 let stock_piece = new_unit
411 .available_stock_pieces
412 .iter_mut()
413 .find(|sp| sp.quantity != Some(0) && bin.matches_stock_piece(sp));
414 let injected_cut_pieces = (&other.bins[cross_src_start..cross_src_end])
415 .iter()
416 .flat_map(Bin::cut_pieces);
417 let num_removed_cut_pieces = bin.remove_cut_pieces(injected_cut_pieces);
418
419 if let (0, Some(stock_piece)) = (num_removed_cut_pieces, stock_piece) {
420 stock_piece.dec_quantity();
423 } else {
424 for cut_piece in bin.cut_pieces() {
428 unused_cut_pieces.insert(cut_piece.into());
429 }
430 new_unit.bins.remove(i);
431 }
432 }
433
434 unused_cut_pieces.retain(|cut_piece| !new_unit.insert_cut_piece(cut_piece, rng));
436 new_unit.unused_cut_pieces = unused_cut_pieces;
437
438 for i in (0..new_unit.bins.len()).rev() {
440 if new_unit.bins[i].cut_pieces().next().is_none() {
441 let bin = &mut new_unit.bins[i];
442 if let Some(ref mut stock_piece) = new_unit
443 .available_stock_pieces
444 .iter_mut()
445 .find(|sp| sp.quantity != Some(0) && bin.matches_stock_piece(sp))
446 {
447 stock_piece.inc_quantity();
449 }
450 new_unit.bins.remove(i);
451 }
452 }
453
454 new_unit
455 }
456
457 fn mutate<R>(&mut self, rng: &mut R)
459 where
460 R: Rng + ?Sized,
461 {
462 if !self.bins.is_empty() && rng.gen_range(0..20) == 1 {
463 self.inversion(rng)
464 }
465 }
466
467 fn inversion<R>(&mut self, rng: &mut R)
469 where
470 R: Rng + ?Sized,
471 {
472 let start = rng.gen_range(0..self.bins.len());
473 let end = rng.gen_range(start..self.bins.len());
474 self.bins[start..end].reverse();
475 }
476}
477
478impl<'a, B> Unit for OptimizerUnit<'a, B>
479where
480 B: Bin + Send + Clone,
481{
482 fn fitness(&self) -> f64 {
483 let fitness = if self.bins.is_empty() {
484 0.0
485 } else {
486 self.bins.iter().fold(0.0, |acc, b| acc + b.fitness()) / self.bins.len() as f64
487 };
488
489 if self.unused_cut_pieces.is_empty() {
490 fitness
491 } else {
492 fitness - 1.0
495 }
496 }
497
498 fn breed_with<R>(&self, other: &OptimizerUnit<'a, B>, rng: &mut R) -> OptimizerUnit<'a, B>
499 where
500 R: Rng + ?Sized,
501 {
502 let mut new_unit = self.crossover(other, rng);
503 new_unit.mutate(rng);
504 new_unit
505 }
506}
507
508#[derive(Debug)]
510pub enum Error {
511 NoFitForCutPiece(CutPiece),
513}
514fn no_fit_for_cut_piece_error(cut_piece: &CutPieceWithId) -> Error {
515 Error::NoFitForCutPiece(CutPiece {
516 quantity: 1,
517 external_id: cut_piece.external_id,
518 length: cut_piece.length,
519 })
520}
521type Result<T> = std::result::Result<T, Error>;
522
523#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
525#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
526pub struct Solution {
527 pub fitness: f64,
530
531 pub stock_pieces: Vec<ResultStockPiece>,
533
534 #[cfg_attr(feature = "serialize", serde(skip))]
535 price: usize,
536}
537
538pub struct Optimizer {
541 stock_pieces: Vec<StockPiece>,
542 cut_pieces: Vec<CutPieceWithId>,
543 cut_width: usize,
544 random_seed: u64,
545 allow_mixed_stock_sizes: bool,
546}
547
548impl Default for Optimizer {
549 fn default() -> Self {
550 Self {
551 stock_pieces: Default::default(),
552 cut_pieces: Default::default(),
553 cut_width: Default::default(),
554 random_seed: Default::default(),
555 allow_mixed_stock_sizes: true,
556 }
557 }
558}
559
560impl Optimizer {
561 pub fn new() -> Self {
563 Default::default()
564 }
565
566 pub fn add_stock_piece(&mut self, stock_piece: StockPiece) -> &mut Self {
571 let mut existing_stock_piece = self
572 .stock_pieces
573 .iter_mut()
574 .find(|sp| sp.length == stock_piece.length && sp.price == stock_piece.price);
575
576 if let Some(ref mut existing_stock_piece) = existing_stock_piece {
577 match (&mut existing_stock_piece.quantity, stock_piece.quantity) {
578 (Some(ref mut existing_quantity), Some(quantity)) => {
579 *existing_quantity += quantity;
582 }
583 _ => {
584 existing_stock_piece.quantity = None;
587 }
588 }
589 } else {
590 self.stock_pieces.push(stock_piece);
592 }
593
594 self
595 }
596
597 pub fn add_stock_pieces<I>(&mut self, stock_pieces: I) -> &mut Self
602 where
603 I: IntoIterator<Item = StockPiece>,
604 {
605 stock_pieces.into_iter().for_each(|sp| {
606 self.add_stock_piece(sp);
607 });
608 self
609 }
610
611 pub fn add_cut_piece(&mut self, cut_piece: CutPiece) -> &mut Self {
613 for _ in 0..cut_piece.quantity {
614 let cut_piece = CutPieceWithId {
615 id: self.cut_pieces.len(),
616 external_id: cut_piece.external_id,
617 length: cut_piece.length,
618 };
619
620 self.cut_pieces.push(cut_piece);
621 }
622
623 self
624 }
625
626 pub fn add_cut_pieces<I>(&mut self, cut_pieces: I) -> &mut Self
628 where
629 I: IntoIterator<Item = CutPiece>,
630 {
631 cut_pieces.into_iter().for_each(|dp| {
632 self.add_cut_piece(dp);
633 });
634 self
635 }
636
637 pub fn set_cut_width(&mut self, cut_width: usize) -> &mut Self {
640 self.cut_width = cut_width;
641 self
642 }
643
644 pub fn set_random_seed(&mut self, seed: u64) -> &mut Self {
647 self.random_seed = seed;
648 self
649 }
650
651 pub fn allow_mixed_stock_sizes(&mut self, allow: bool) -> &mut Self {
655 self.allow_mixed_stock_sizes = allow;
656 self
657 }
658
659 pub fn optimize<F>(&self, progress_callback: F) -> Result<Solution>
661 where
662 F: Fn(f64),
663 {
664 if self.cut_pieces.is_empty() {
666 return Ok(Solution {
667 fitness: 1.0,
668 stock_pieces: Vec::new(),
669 price: 0,
670 });
671 }
672
673 let size_set: FnvHashSet<usize> = self.stock_pieces.iter().map(|sp| sp.length).collect();
674
675 let num_runs = size_set.len() + if self.allow_mixed_stock_sizes { 1 } else { 0 };
676 let callback = |progress| {
677 progress_callback(progress / num_runs as f64);
678 };
679
680 let mut best_result = if self.allow_mixed_stock_sizes {
681 self.optimize_with_stock_pieces::<BasicBin, _>(&self.stock_pieces.clone(), &callback)
683 } else {
684 Err(no_fit_for_cut_piece_error(&self.cut_pieces[0]))
689 };
690
691 for (i, length) in size_set.iter().enumerate() {
694 let stock_pieces: Vec<StockPiece> = self
695 .stock_pieces
696 .iter()
697 .filter(|sp| sp.length == *length)
698 .cloned()
699 .collect();
700
701 let completed_runs = i + 1;
702 if let Ok(solution) =
703 self.optimize_with_stock_pieces::<BasicBin, _>(&stock_pieces, &|progress| {
704 progress_callback((completed_runs as f64 + progress) / num_runs as f64);
705 })
706 {
707 match best_result {
708 Ok(ref best_solution) => {
709 if solution.fitness < 0.0 || best_solution.fitness < 0.0 {
712 if solution.fitness > best_solution.fitness {
713 best_result = Ok(solution);
714 }
715 } else if solution.price < best_solution.price
716 || (solution.price == best_solution.price
717 && solution.fitness > best_solution.fitness)
718 {
719 best_result = Ok(solution);
720 }
721 }
722 Err(_) => best_result = Ok(solution),
723 }
724 }
725 }
726
727 if let Ok(ref mut solution) = &mut best_result {
728 solution
729 .stock_pieces
730 .sort_by_key(|p| cmp::Reverse(p.length));
731 };
732
733 best_result
734 }
735
736 fn optimize_with_stock_pieces<B, F>(
737 &self,
738 stock_pieces: &[StockPiece],
739 progress_callback: &F,
740 ) -> Result<Solution>
741 where
742 B: Bin + Clone + Send + Into<ResultStockPiece>,
743 F: Fn(f64),
744 {
745 let cut_pieces: Vec<&CutPieceWithId> = self.cut_pieces.iter().collect();
746
747 let units: Vec<OptimizerUnit<B>> = OptimizerUnit::generate_initial_units(
748 stock_pieces,
749 cut_pieces,
750 self.cut_width,
751 self.random_seed,
752 )?;
753
754 let population_size = units.len();
755 let mut result_units = Population::new(units)
756 .set_size(population_size)
757 .set_rand_seed(self.random_seed)
758 .set_breed_factor(0.5)
759 .set_survival_factor(0.6)
760 .epochs(100, progress_callback)
761 .finish();
762
763 let best_unit = &mut result_units[0];
764 if !best_unit.unused_cut_pieces.is_empty() {
765 return Err(no_fit_for_cut_piece_error(
766 best_unit.unused_cut_pieces.iter().next().unwrap(),
767 ));
768 }
769
770 let fitness = best_unit.fitness();
771 let price = best_unit.bins.iter().map(|bin| bin.price()).sum();
772
773 let used_stock_pieces: Vec<ResultStockPiece> =
774 best_unit.bins.drain(..).map(Into::into).collect();
775
776 Ok(Solution {
777 fitness,
778 stock_pieces: used_stock_pieces,
779 price,
780 })
781 }
782}