cut_optimizer_2d/
lib.rs

1//! cut-optimizer-2d is an optimizer library that attempts layout rectangular cut pieces from stock pieces in a
2//! way that gives the least waste. It uses genetic algorithms and multiple heuristics to solve the problem.
3
4#![deny(missing_docs)]
5
6mod genetic;
7mod guillotine;
8mod maxrects;
9
10#[cfg(test)]
11mod tests;
12
13use genetic::population::Population;
14use genetic::unit::Unit;
15use guillotine::GuillotineBin;
16use maxrects::MaxRectsBin;
17
18use fnv::FnvHashSet;
19use rand::prelude::*;
20use rand::seq::SliceRandom;
21
22use std::borrow::Borrow;
23use std::cmp;
24use std::hash::{Hash, Hasher};
25
26#[cfg(feature = "serialize")]
27use serde::{Deserialize, Serialize};
28
29/// Indicates the linear direction of a pattern, grain, etc.
30#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
31#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
32#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
33pub enum PatternDirection {
34    /// No pattern
35    None,
36
37    /// Linear pattern that runs parallel to the width
38    ParallelToWidth,
39
40    /// Linear pattern that runs parallel to the length
41    ParallelToLength,
42}
43
44impl PatternDirection {
45    /// Returns the opposite orientation of this `PatternDirection`.
46    fn rotated(self) -> PatternDirection {
47        match self {
48            PatternDirection::None => PatternDirection::None,
49            PatternDirection::ParallelToWidth => PatternDirection::ParallelToLength,
50            PatternDirection::ParallelToLength => PatternDirection::ParallelToWidth,
51        }
52    }
53}
54
55impl Default for PatternDirection {
56    fn default() -> Self {
57        PatternDirection::None
58    }
59}
60
61/// A rectangular piece that needs to be cut from a stock piece.
62#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
63#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
64#[derive(Clone, Debug)]
65pub struct CutPiece {
66    /// Quantity of this cut piece.
67    pub quantity: usize,
68
69    /// ID to be used by the caller to match up result cut pieces
70    /// with the original cut piece. This ID has no meaning to the
71    /// optimizer so it can be set to `None` if not needed.
72    pub external_id: Option<usize>,
73
74    /// Width of this rectangular cut piece.
75    pub width: usize,
76
77    /// Length of this rectangular cut piece.
78    pub length: usize,
79
80    /// Pattern direction of this cut piece.
81    pub pattern_direction: PatternDirection,
82
83    /// Whether or not the optimizer is allowed to rotate this piece to make it fit.
84    pub can_rotate: bool,
85}
86
87#[derive(Clone, Debug)]
88pub(crate) struct CutPieceWithId {
89    pub(crate) id: usize,
90    pub(crate) external_id: Option<usize>,
91    pub(crate) width: usize,
92    pub(crate) length: usize,
93    pub(crate) pattern_direction: PatternDirection,
94    pub(crate) can_rotate: bool,
95}
96
97impl Hash for CutPieceWithId {
98    fn hash<H: Hasher>(&self, state: &mut H) {
99        self.id.hash(state);
100    }
101}
102impl PartialEq for CutPieceWithId {
103    fn eq(&self, other: &CutPieceWithId) -> bool {
104        self.id == other.id
105    }
106}
107impl Eq for CutPieceWithId {}
108
109#[derive(Clone, Debug)]
110pub(crate) struct UsedCutPiece {
111    pub(crate) id: usize,
112    pub(crate) external_id: Option<usize>,
113    pub(crate) rect: Rect,
114    pub(crate) pattern_direction: PatternDirection,
115    pub(crate) is_rotated: bool,
116    pub(crate) can_rotate: bool,
117}
118
119impl PartialEq for UsedCutPiece {
120    fn eq(&self, other: &UsedCutPiece) -> bool {
121        self.id == other.id
122    }
123}
124impl Eq for UsedCutPiece {}
125
126impl From<&UsedCutPiece> for CutPieceWithId {
127    fn from(used_cut_piece: &UsedCutPiece) -> Self {
128        let (width, length, pattern_direction) = if used_cut_piece.is_rotated {
129            (
130                used_cut_piece.rect.length,
131                used_cut_piece.rect.width,
132                used_cut_piece.pattern_direction.rotated(),
133            )
134        } else {
135            (
136                used_cut_piece.rect.width,
137                used_cut_piece.rect.length,
138                used_cut_piece.pattern_direction,
139            )
140        };
141
142        Self {
143            id: used_cut_piece.id,
144            external_id: used_cut_piece.external_id,
145            width,
146            length,
147            can_rotate: used_cut_piece.can_rotate,
148            pattern_direction,
149        }
150    }
151}
152
153impl From<&UsedCutPiece> for ResultCutPiece {
154    fn from(used_cut_piece: &UsedCutPiece) -> Self {
155        Self {
156            external_id: used_cut_piece.external_id,
157            x: used_cut_piece.rect.x,
158            y: used_cut_piece.rect.y,
159            width: used_cut_piece.rect.width,
160            length: used_cut_piece.rect.length,
161            pattern_direction: used_cut_piece.pattern_direction,
162            is_rotated: used_cut_piece.is_rotated,
163        }
164    }
165}
166
167/// A cut piece that has been placed in a solution by the optimizer.
168#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
169#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
170#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct ResultCutPiece {
172    /// ID that matches the one on the cut piece that was passed to the optimizer.
173    pub external_id: Option<usize>,
174
175    /// X location of the left side of this cut piece within the stock piece.
176    pub x: usize,
177
178    /// Y location of the (bottom or top) side of this cut piece within the stock piece.
179    pub y: usize,
180
181    /// Width of this cut piece.
182    pub width: usize,
183
184    /// Length of this cut piece.
185    pub length: usize,
186
187    /// Pattern direction of this cut piece.
188    pub pattern_direction: PatternDirection,
189
190    /// Whether or not this cut piece was rotated 90 degrees by the optimizer from it's original
191    /// oriorientation.
192    pub is_rotated: bool,
193}
194
195/// A rectangular stock piece that is available to cut one or more
196/// cut pieces from.
197#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
198#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
199#[derive(Hash, Copy, Clone, Debug, Eq, PartialEq)]
200pub struct StockPiece {
201    /// Width of rectangular stock piece.
202    pub width: usize,
203
204    /// Length of rectangular stock piece.
205    pub length: usize,
206
207    /// Pattern direction of stock piece.
208    pub pattern_direction: PatternDirection,
209
210    /// Price to use to optimize for price when not all stock pieces are the same price per unit
211    /// area. If optimizing for less waste instead, price can be set to 0 for all stock pieces.
212    pub price: usize,
213
214    /// Quantity of this stock piece available for optimization. `None` means infinite quantity.
215    pub quantity: Option<usize>,
216}
217
218impl StockPiece {
219    /// Checks whether of not the cut piece fits within the bounds of this stock piece.
220    fn fits_cut_piece(&self, cut_piece: &CutPieceWithId) -> bool {
221        let rect = Rect {
222            x: 0,
223            y: 0,
224            width: self.width,
225            length: self.length,
226        };
227
228        rect.fit_cut_piece(self.pattern_direction, cut_piece, false) != Fit::None
229    }
230
231    /// Decrement the quantity of this stock piece. If quantity is `None` it will remain `None`.
232    fn dec_quantity(&mut self) {
233        if let Some(ref mut quantity) = self.quantity {
234            *quantity -= 1;
235        }
236    }
237
238    /// Increment the quantity of this stock piece. If quantity is `None` it will remain `None`.
239    fn inc_quantity(&mut self) {
240        if let Some(ref mut quantity) = self.quantity {
241            *quantity += 1;
242        }
243    }
244}
245
246/// Stock piece that was used by the optimizer to get one or more cut pieces.
247#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
248#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
249#[derive(Clone, Debug)]
250pub struct ResultStockPiece {
251    /// Width of this stock piece.
252    pub width: usize,
253
254    /// Length of this stock piece.
255    pub length: usize,
256
257    /// Pattern direction of this stock piece.
258    pub pattern_direction: PatternDirection,
259
260    /// Cut pieces to cut from this stock piece.
261    pub cut_pieces: Vec<ResultCutPiece>,
262
263    /// Waste pieces that remain after cutting the cut pieces.
264    pub waste_pieces: Vec<Rect>,
265
266    /// Price of stock piece.
267    pub price: usize,
268}
269
270/// A rectangle
271#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
272#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
273#[derive(Copy, Clone, Debug, Default)]
274pub struct Rect {
275    /// X location of this rectangle.
276    x: usize,
277
278    /// Y location of this rectangle.
279    y: usize,
280
281    /// Width of this rectangle.
282    width: usize,
283
284    /// Length of this rectangle.
285    length: usize,
286}
287
288impl Rect {
289    fn fit_cut_piece(
290        &self,
291        pattern_direction: PatternDirection,
292        cut_piece: &CutPieceWithId,
293        prefer_rotated: bool,
294    ) -> Fit {
295        let upright_fit = if cut_piece.pattern_direction == pattern_direction {
296            if cut_piece.width == self.width && cut_piece.length == self.length {
297                Some(Fit::UprightExact)
298            } else if cut_piece.width <= self.width && cut_piece.length <= self.length {
299                Some(Fit::Upright)
300            } else {
301                None
302            }
303        } else {
304            None
305        };
306
307        let rotated_fit =
308            if cut_piece.can_rotate && cut_piece.pattern_direction.rotated() == pattern_direction {
309                if cut_piece.length == self.width && cut_piece.width == self.length {
310                    Some(Fit::RotatedExact)
311                } else if cut_piece.length <= self.width && cut_piece.width <= self.length {
312                    Some(Fit::Rotated)
313                } else {
314                    None
315                }
316            } else {
317                None
318            };
319
320        match (upright_fit, rotated_fit) {
321            (Some(upright_fit), Some(rotated_fit)) => {
322                if prefer_rotated {
323                    rotated_fit
324                } else {
325                    upright_fit
326                }
327            }
328            (Some(upright_fit), None) => upright_fit,
329            (None, Some(rotated_fit)) => rotated_fit,
330            (None, None) => Fit::None,
331        }
332    }
333
334    fn contains(&self, rect: &Rect) -> bool {
335        rect.x >= self.x
336            && rect.x + rect.width <= self.x + self.width
337            && rect.y >= self.y
338            && rect.y + rect.length <= self.y + self.length
339    }
340}
341
342impl From<&ResultCutPiece> for Rect {
343    fn from(cut_piece: &ResultCutPiece) -> Self {
344        Self {
345            x: cut_piece.x,
346            y: cut_piece.y,
347            width: cut_piece.width,
348            length: cut_piece.length,
349        }
350    }
351}
352
353#[derive(Copy, Clone, PartialEq, Eq)]
354enum Fit {
355    None,
356    UprightExact,
357    RotatedExact,
358    Upright,
359    Rotated,
360}
361
362impl Fit {
363    fn is_none(self) -> bool {
364        self == Fit::None
365    }
366
367    fn is_upright(self) -> bool {
368        self == Fit::Upright || self == Fit::UprightExact
369    }
370
371    fn is_rotated(self) -> bool {
372        self == Fit::Rotated || self == Fit::RotatedExact
373    }
374}
375
376/// Represents a bin used for bin-packing.
377trait Bin {
378    /// Heuristic used for inserting `CutPiece`s.
379    type Heuristic;
380
381    /// Creates a new `Bin`.
382    fn new(
383        width: usize,
384        length: usize,
385        blade_width: usize,
386        pattern_direction: PatternDirection,
387        price: usize,
388    ) -> Self;
389
390    /// Computes the fitness of this `Bin` on a scale of 0.0 to 1.0, with 1.0 being the most fit.
391    fn fitness(&self) -> f64;
392
393    fn price(&self) -> usize;
394
395    /// Removes `UsedCutPiece`s from this `Bin` and returns how many were removed.
396    fn remove_cut_pieces<I>(&mut self, cut_pieces: I) -> usize
397    where
398        I: Iterator,
399        I::Item: Borrow<UsedCutPiece>;
400
401    /// Returns an iterator over the `UsedCutPiece`s in this `Bin`.
402    fn cut_pieces(&self) -> std::slice::Iter<'_, UsedCutPiece>;
403
404    /// Returns the possible heuristics that can be passed to `insert_cut_piece_with_heuristic`.
405    fn possible_heuristics() -> Vec<Self::Heuristic>;
406
407    /// Inserts the `CutPieceWithId` into this `Bin` using the specified heuristic. Returns whether
408    /// the insert succeeded.
409    fn insert_cut_piece_with_heuristic(
410        &mut self,
411        cut_piece: &CutPieceWithId,
412        heuristic: &Self::Heuristic,
413    ) -> bool;
414
415    /// Inserts the `CutPieceWithId` into this `Bin` using a random heuristic. Returns whether
416    /// the insert succeeded.
417    fn insert_cut_piece_random_heuristic<R>(
418        &mut self,
419        cut_piece: &CutPieceWithId,
420        rng: &mut R,
421    ) -> bool
422    where
423        R: Rng + ?Sized;
424
425    /// Returns whether the `StockPiece` is equivalent to this `Bin`.
426    fn matches_stock_piece(&self, stock_piece: &StockPiece) -> bool;
427}
428
429struct OptimizerUnit<'a, B>
430where
431    B: Bin,
432{
433    bins: Vec<B>,
434
435    // All of the possible stock pieces. It remains constant.
436    possible_stock_pieces: &'a [StockPiece],
437
438    // Stock pieces that are currently available to use for new bins.
439    available_stock_pieces: Vec<StockPiece>,
440
441    // Cut pieces that couldn't be added to bins.
442    unused_cut_pieces: FnvHashSet<CutPieceWithId>,
443
444    blade_width: usize,
445}
446
447impl<'a, B> Clone for OptimizerUnit<'a, B>
448where
449    B: Bin + Clone,
450{
451    fn clone(&self) -> Self {
452        Self {
453            bins: self.bins.clone(),
454            possible_stock_pieces: self.possible_stock_pieces,
455            available_stock_pieces: self.available_stock_pieces.clone(),
456            unused_cut_pieces: self.unused_cut_pieces.clone(),
457            blade_width: self.blade_width,
458        }
459    }
460}
461
462impl<'a, B> OptimizerUnit<'a, B>
463where
464    B: Bin,
465{
466    fn with_random_heuristics<R>(
467        possible_stock_pieces: &'a [StockPiece],
468        cut_pieces: &[&CutPieceWithId],
469        blade_width: usize,
470        rng: &mut R,
471    ) -> Result<OptimizerUnit<'a, B>>
472    where
473        R: Rng + ?Sized,
474    {
475        let mut unit = OptimizerUnit {
476            bins: Vec::new(),
477            possible_stock_pieces,
478            available_stock_pieces: possible_stock_pieces.to_vec(),
479            unused_cut_pieces: Default::default(),
480            blade_width,
481        };
482
483        for cut_piece in cut_pieces {
484            if !unit.first_fit_random_heuristics(cut_piece, rng) {
485                unit.unused_cut_pieces.insert((*cut_piece).clone());
486            }
487        }
488
489        Ok(unit)
490    }
491
492    fn with_heuristic<R>(
493        possible_stock_pieces: &'a [StockPiece],
494        cut_pieces: &[&CutPieceWithId],
495        blade_width: usize,
496        heuristic: &B::Heuristic,
497        rng: &mut R,
498    ) -> Result<OptimizerUnit<'a, B>>
499    where
500        R: Rng + ?Sized,
501    {
502        let mut unit = OptimizerUnit {
503            bins: Vec::new(),
504            possible_stock_pieces,
505            available_stock_pieces: possible_stock_pieces.to_vec(),
506            unused_cut_pieces: Default::default(),
507            blade_width,
508        };
509
510        for cut_piece in cut_pieces {
511            if !unit.first_fit_with_heuristic(cut_piece, heuristic, rng) {
512                unit.unused_cut_pieces.insert((*cut_piece).clone());
513            }
514        }
515
516        Ok(unit)
517    }
518
519    pub(crate) fn generate_initial_units(
520        possible_stock_pieces: &'a [StockPiece],
521        mut cut_pieces: Vec<&CutPieceWithId>,
522        blade_width: usize,
523        random_seed: u64,
524    ) -> Result<Vec<OptimizerUnit<'a, B>>> {
525        let mut set = FnvHashSet::default();
526        for cut_piece in &cut_pieces {
527            set.insert((
528                cut_piece.width,
529                cut_piece.length,
530                cut_piece.can_rotate,
531                cut_piece.pattern_direction,
532            ));
533        }
534        let unique_cut_pieces = set.len();
535
536        let possible_heuristics = B::possible_heuristics();
537
538        let num_units = if cut_pieces.len() < 3 {
539            possible_heuristics.len()
540        } else {
541            let denom = if cut_pieces.len() > 1 {
542                (cut_pieces.len() as f64).log10()
543            } else {
544                1.0
545            };
546
547            cmp::max(
548                possible_heuristics.len() * 3,
549                (cut_pieces.len() as f64 / denom + ((unique_cut_pieces - 1) * 10) as f64) as usize,
550            )
551        };
552        let mut units = Vec::with_capacity(num_units);
553        let mut rng: StdRng = SeedableRng::seed_from_u64(random_seed);
554
555        cut_pieces.sort_by_key(|p| cmp::Reverse((p.width, p.length)));
556        for heuristic in &possible_heuristics {
557            units.push(OptimizerUnit::with_heuristic(
558                possible_stock_pieces,
559                &cut_pieces,
560                blade_width,
561                heuristic,
562                &mut rng,
563            )?);
564        }
565
566        if cut_pieces.len() > 2 {
567            for heuristic in &possible_heuristics {
568                cut_pieces.shuffle(&mut rng);
569                units.push(OptimizerUnit::with_heuristic(
570                    possible_stock_pieces,
571                    &cut_pieces,
572                    blade_width,
573                    heuristic,
574                    &mut rng,
575                )?);
576            }
577
578            for _ in 0..num_units - units.len() {
579                cut_pieces.shuffle(&mut rng);
580                units.push(OptimizerUnit::with_random_heuristics(
581                    possible_stock_pieces,
582                    &cut_pieces,
583                    blade_width,
584                    &mut rng,
585                )?);
586            }
587        }
588        Ok(units)
589    }
590
591    fn first_fit_random_heuristics<R>(&mut self, cut_piece: &CutPieceWithId, rng: &mut R) -> bool
592    where
593        R: Rng + ?Sized,
594    {
595        for bin in self.bins.iter_mut() {
596            if bin.insert_cut_piece_random_heuristic(cut_piece, rng) {
597                return true;
598            }
599        }
600
601        self.add_to_new_bin(cut_piece, rng)
602    }
603
604    fn first_fit_with_heuristic<R>(
605        &mut self,
606        cut_piece: &CutPieceWithId,
607        heuristic: &B::Heuristic,
608        rng: &mut R,
609    ) -> bool
610    where
611        R: Rng + ?Sized,
612    {
613        for bin in self.bins.iter_mut() {
614            if bin.insert_cut_piece_with_heuristic(cut_piece, heuristic) {
615                return true;
616            }
617        }
618
619        self.add_to_new_bin(cut_piece, rng)
620    }
621
622    fn add_to_new_bin<R>(&mut self, cut_piece: &CutPieceWithId, rng: &mut R) -> bool
623    where
624        R: Rng + ?Sized,
625    {
626        let stock_pieces = self
627            .available_stock_pieces
628            .iter_mut()
629            .filter(|stock_piece| {
630                stock_piece.quantity != Some(0) && stock_piece.fits_cut_piece(cut_piece)
631            });
632
633        match stock_pieces.choose(rng) {
634            Some(stock_piece) => {
635                stock_piece.dec_quantity();
636
637                let mut bin = B::new(
638                    stock_piece.width,
639                    stock_piece.length,
640                    self.blade_width,
641                    stock_piece.pattern_direction,
642                    stock_piece.price,
643                );
644                if !bin.insert_cut_piece_random_heuristic(cut_piece, rng) {
645                    return false;
646                }
647                self.bins.push(bin);
648                true
649            }
650            None => false,
651        }
652    }
653
654    fn crossover<R>(&self, other: &OptimizerUnit<'a, B>, rng: &mut R) -> OptimizerUnit<'a, B>
655    where
656        R: Rng + ?Sized,
657        B: Clone,
658    {
659        // If there aren't multiple bins we can't do a crossover,
660        // so just return a clone of this unit.
661        if self.bins.len() < 2 && other.bins.len() < 2 {
662            return self.clone();
663        }
664
665        // Randomly select insertion point and range of bins to inject into the new unit.
666        let cross_dest = rng.gen_range(0..=self.bins.len());
667        let cross_src_start = rng.gen_range(0..other.bins.len());
668        let cross_src_end = rng.gen_range(cross_src_start + 1..=other.bins.len());
669
670        // Create a new unit with the bins of `self` and some of the bins of `other` injected in.
671        let mut new_unit = OptimizerUnit {
672            bins: (&self.bins[..cross_dest])
673                .iter()
674                .chain((&other.bins[cross_src_start..cross_src_end]).iter())
675                .chain((&self.bins[cross_dest..]).iter())
676                .cloned()
677                .collect(),
678            possible_stock_pieces: self.possible_stock_pieces,
679            // Make all possible stock pieces available initially. Quantities will be updated below.
680            available_stock_pieces: self.possible_stock_pieces.to_vec(),
681            // Start with no unused cut pieces, and update below.
682            unused_cut_pieces: Default::default(),
683            blade_width: self.blade_width,
684        };
685
686        let mut unused_cut_pieces = self.unused_cut_pieces.clone();
687
688        // Update available stock piece quantities based on the injected bins.
689        other.bins[cross_src_start..cross_src_end]
690            .iter()
691            .for_each(|bin| {
692                if let Some(ref mut stock_piece) = new_unit
693                    .available_stock_pieces
694                    .iter_mut()
695                    .find(|sp| bin.matches_stock_piece(sp))
696                {
697                    // Remove the injected cut pieces from the unused set.
698                    for cut_piece in bin.cut_pieces() {
699                        unused_cut_pieces.remove(&cut_piece.into());
700                    }
701                    stock_piece.dec_quantity();
702                } else {
703                    panic!("Attempt to inject invalid bin in crossover operation. This shouldn't happen, and means there is a bug in the code.");
704                }
705            });
706
707        // Remove injected cut pieces from all non-injected bins,
708        // and update the available stock piece quantities.
709        for i in (0..cross_dest)
710            .chain((cross_dest + cross_src_end - cross_src_start)..new_unit.bins.len())
711            .rev()
712        {
713            let bin = &mut new_unit.bins[i];
714            let stock_piece = new_unit
715                .available_stock_pieces
716                .iter_mut()
717                .find(|sp| sp.quantity != Some(0) && bin.matches_stock_piece(sp));
718            let injected_cut_pieces = (&other.bins[cross_src_start..cross_src_end])
719                .iter()
720                .flat_map(Bin::cut_pieces);
721            let num_removed_cut_pieces = bin.remove_cut_pieces(injected_cut_pieces);
722
723            if let (0, Some(stock_piece)) = (num_removed_cut_pieces, stock_piece) {
724                // No cut pieces were removed from this bin, so we'll keep it
725                // and decrement the quantity of the available stock piece.
726                stock_piece.dec_quantity();
727            } else {
728                // Either there were no available stock pieces, or there
729                // were cut pieces removed from this bin so we'll remove it
730                // and add its cut pieces to the unused set.
731                for cut_piece in bin.cut_pieces() {
732                    unused_cut_pieces.insert(cut_piece.into());
733                }
734                new_unit.bins.remove(i);
735            }
736        }
737
738        // Try to add all unused cut pieces.
739        unused_cut_pieces.retain(|cut_piece| !new_unit.first_fit_random_heuristics(cut_piece, rng));
740        new_unit.unused_cut_pieces = unused_cut_pieces;
741
742        // Remove bins that don't have cut pieces.
743        for i in (0..new_unit.bins.len()).rev() {
744            if new_unit.bins[i].cut_pieces().next().is_none() {
745                let bin = &mut new_unit.bins[i];
746                if let Some(ref mut stock_piece) = new_unit
747                    .available_stock_pieces
748                    .iter_mut()
749                    .find(|sp| sp.quantity != Some(0) && bin.matches_stock_piece(sp))
750                {
751                    // Make this stock piece available to use again.
752                    stock_piece.inc_quantity();
753                }
754                new_unit.bins.remove(i);
755            }
756        }
757
758        new_unit
759    }
760
761    // Randomly apply a mutation to this unit.
762    fn mutate<R>(&mut self, rng: &mut R)
763    where
764        R: Rng + ?Sized,
765    {
766        if !self.bins.is_empty() && rng.gen_range(0..20) == 1 {
767            self.inversion(rng)
768        }
769    }
770
771    // Reverse order of a random range of bins.
772    fn inversion<R>(&mut self, rng: &mut R)
773    where
774        R: Rng + ?Sized,
775    {
776        let start = rng.gen_range(0..self.bins.len());
777        let end = rng.gen_range(start..self.bins.len());
778        self.bins[start..end].reverse();
779    }
780}
781
782impl<'a, B> Unit for OptimizerUnit<'a, B>
783where
784    B: Bin + Send + Clone,
785{
786    fn fitness(&self) -> f64 {
787        let fitness = if self.bins.is_empty() {
788            0.0
789        } else {
790            self.bins.iter().fold(0.0, |acc, b| acc + b.fitness()) / self.bins.len() as f64
791        };
792
793        if self.unused_cut_pieces.is_empty() {
794            fitness
795        } else {
796            // If there are unused cut pieces, the fitness is below 0 because it's not a valid
797            // solution.
798            fitness - 1.0
799        }
800    }
801
802    fn breed_with<R>(&self, other: &OptimizerUnit<'a, B>, rng: &mut R) -> OptimizerUnit<'a, B>
803    where
804        R: Rng + ?Sized,
805    {
806        let mut new_unit = self.crossover(other, rng);
807        new_unit.mutate(rng);
808        new_unit
809    }
810}
811
812/// Error while optimizing.
813#[derive(Debug)]
814pub enum Error {
815    /// There was no stock piece that could contain this demand piece.
816    NoFitForCutPiece(CutPiece),
817}
818fn no_fit_for_cut_piece_error(cut_piece: &CutPieceWithId) -> Error {
819    Error::NoFitForCutPiece(CutPiece {
820        quantity: 1,
821        external_id: cut_piece.external_id,
822        width: cut_piece.width,
823        length: cut_piece.length,
824        can_rotate: cut_piece.can_rotate,
825        pattern_direction: cut_piece.pattern_direction,
826    })
827}
828type Result<T> = std::result::Result<T, Error>;
829
830/// A valid solution to an optimization.
831#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
832#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
833pub struct Solution {
834    /// Fitness score for this solution.
835    /// Ranges between 0.0 and 1.0 inclusive, with 1.0 being a perfect solution with no waste.
836    pub fitness: f64,
837
838    /// The stock pieces that were used for this solution, each containing the demand piece layout.
839    pub stock_pieces: Vec<ResultStockPiece>,
840
841    #[cfg_attr(feature = "serialize", serde(skip))]
842    price: usize,
843}
844
845/// Optimizer for optimizing rectangular cut pieces from rectangular
846/// stock pieces.
847pub struct Optimizer {
848    stock_pieces: Vec<StockPiece>,
849    cut_pieces: Vec<CutPieceWithId>,
850    cut_width: usize,
851    random_seed: u64,
852    allow_mixed_stock_sizes: bool,
853}
854
855impl Default for Optimizer {
856    fn default() -> Self {
857        Self {
858            stock_pieces: Default::default(),
859            cut_pieces: Default::default(),
860            cut_width: Default::default(),
861            random_seed: Default::default(),
862            allow_mixed_stock_sizes: true,
863        }
864    }
865}
866
867impl Optimizer {
868    /// Create a new optimizer.
869    pub fn new() -> Self {
870        Default::default()
871    }
872
873    /// Add a stock piece that the optimizer can use to optimize cut pieces.
874    /// If the same stock piece is added multiple times, the quantities will be
875    /// summed up. If any have a `None` quantity, the quantity on other equivalent
876    /// pieces will be ignored.
877    pub fn add_stock_piece(&mut self, stock_piece: StockPiece) -> &mut Self {
878        let mut existing_stock_piece = self.stock_pieces.iter_mut().find(|sp| {
879            sp.width == stock_piece.width
880                && sp.length == stock_piece.length
881                && sp.pattern_direction == stock_piece.pattern_direction
882                && sp.price == stock_piece.price
883        });
884
885        if let Some(ref mut existing_stock_piece) = existing_stock_piece {
886            match (&mut existing_stock_piece.quantity, stock_piece.quantity) {
887                (Some(ref mut existing_quantity), Some(quantity)) => {
888                    // If there is already a stock piece that is the same except the quantity, add
889                    // to the quantity.
890                    *existing_quantity += quantity;
891                }
892                _ => {
893                    // `None` quantity means infinite, so if either is `None` we want it to be
894                    // `None`.
895                    existing_stock_piece.quantity = None;
896                }
897            }
898        } else {
899            // A stock piece like this hasn't yet been added so let's do it.
900            self.stock_pieces.push(stock_piece);
901        }
902
903        self
904    }
905
906    /// Add a stock pieces that the optimizer can use to optimize cut pieces.
907    /// If the same stock piece is added multiple times, the quantities will be
908    /// summed up. If any have a `None` quantity, the quantity on other equivalent
909    /// pieces will be ignored.
910    pub fn add_stock_pieces<I>(&mut self, stock_pieces: I) -> &mut Self
911    where
912        I: IntoIterator<Item = StockPiece>,
913    {
914        stock_pieces.into_iter().for_each(|sp| {
915            self.add_stock_piece(sp);
916        });
917        self
918    }
919
920    /// Add a desired cut piece that you need cut from a stock piece.
921    pub fn add_cut_piece(&mut self, cut_piece: CutPiece) -> &mut Self {
922        for _ in 0..cut_piece.quantity {
923            let cut_piece = CutPieceWithId {
924                id: self.cut_pieces.len(),
925                external_id: cut_piece.external_id,
926                width: cut_piece.width,
927                length: cut_piece.length,
928                pattern_direction: cut_piece.pattern_direction,
929                can_rotate: cut_piece.can_rotate,
930            };
931
932            self.cut_pieces.push(cut_piece);
933        }
934
935        self
936    }
937
938    /// Add desired cut pieces that you need cut from a stock piece.
939    pub fn add_cut_pieces<I>(&mut self, cut_pieces: I) -> &mut Self
940    where
941        I: IntoIterator<Item = CutPiece>,
942    {
943        cut_pieces.into_iter().for_each(|dp| {
944            self.add_cut_piece(dp);
945        });
946        self
947    }
948
949    /// Set the width of the cut to use between cut pieces. This could
950    /// represent blade or kerf thickness.
951    pub fn set_cut_width(&mut self, cut_width: usize) -> &mut Self {
952        self.cut_width = cut_width;
953        self
954    }
955
956    /// Set the random seed used by the genetic algorithms in the optimizer. Using
957    /// the same random seed will give you the same result for the same input.
958    pub fn set_random_seed(&mut self, seed: u64) -> &mut Self {
959        self.random_seed = seed;
960        self
961    }
962
963    /// Set whether the optimizer should allow mixed sized stock pieces in the results.
964    /// If set to false, and multiple stock sizes are given, only one stock size will be used in
965    /// the results.
966    pub fn allow_mixed_stock_sizes(&mut self, allow: bool) -> &mut Self {
967        self.allow_mixed_stock_sizes = allow;
968        self
969    }
970
971    /// Optimize in a way where each cut piece can be cut out using only guillotine cuts,
972    /// where each cut extends from one side to the other.
973    ///
974    /// This method is suitable for cutting with a panel saw.
975    pub fn optimize_guillotine<F>(&self, progress_callback: F) -> Result<Solution>
976    where
977        F: Fn(f64),
978    {
979        self.optimize::<GuillotineBin, F>(progress_callback)
980    }
981
982    /// Optimize without the requirement of guillotine cuts. Cuts can start and stop in the middle
983    /// of the stock piece.
984    ///
985    /// This method is suitable for cutting on a CNC.
986    pub fn optimize_nested<F>(&self, progress_callback: F) -> Result<Solution>
987    where
988        F: Fn(f64),
989    {
990        self.optimize::<MaxRectsBin, F>(progress_callback)
991    }
992
993    fn optimize<B, F>(&self, progress_callback: F) -> Result<Solution>
994    where
995        B: Bin + Clone + Send + Into<ResultStockPiece>,
996        F: Fn(f64),
997    {
998        // If there are no cut pieces, there's nothing to optimize.
999        if self.cut_pieces.is_empty() {
1000            return Ok(Solution {
1001                fitness: 1.0,
1002                stock_pieces: Vec::new(),
1003                price: 0,
1004            });
1005        }
1006
1007        let size_set: FnvHashSet<(usize, usize)> = self
1008            .stock_pieces
1009            .iter()
1010            .map(|sp| (sp.width, sp.length))
1011            .collect();
1012
1013        let num_runs = size_set.len() + if self.allow_mixed_stock_sizes { 1 } else { 0 };
1014        let callback = |progress| {
1015            progress_callback(progress / num_runs as f64);
1016        };
1017
1018        let mut best_result = if self.allow_mixed_stock_sizes {
1019            // Optimize with all stock sizes
1020            self.optimize_with_stock_pieces::<B, _>(&self.stock_pieces.clone(), &callback)
1021        } else {
1022            // We're not allowing mixed sizes so just give an error result
1023            // here. Each stock size will be optimized separately below.
1024            // Note: it's safe to assume `self.cut_pieces` isn't empty because
1025            // that's checked at the beginning of this function.
1026            Err(no_fit_for_cut_piece_error(&self.cut_pieces[0]))
1027        };
1028
1029        // Optimize each stock size separately and see if any have better result than
1030        // when optimizing with all stock sizes.
1031        for (i, (width, length)) in size_set.iter().enumerate() {
1032            let stock_pieces: Vec<StockPiece> = self
1033                .stock_pieces
1034                .iter()
1035                .filter(|sp| sp.width == *width && sp.length == *length)
1036                .cloned()
1037                .collect();
1038
1039            let completed_runs = i + 1;
1040            if let Ok(solution) =
1041                self.optimize_with_stock_pieces::<B, _>(&stock_pieces, &|progress| {
1042                    progress_callback((completed_runs as f64 + progress) / num_runs as f64);
1043                })
1044            {
1045                match best_result {
1046                    Ok(ref best_solution) => {
1047                        // Use the lower-priced solution, but if the prices are the same, use the
1048                        // solution with the higher fitness score.
1049                        if solution.fitness < 0.0 || best_solution.fitness < 0.0 {
1050                            if solution.fitness > best_solution.fitness {
1051                                best_result = Ok(solution);
1052                            }
1053                        } else if solution.price < best_solution.price
1054                            || (solution.price == best_solution.price
1055                                && solution.fitness > best_solution.fitness)
1056                        {
1057                            best_result = Ok(solution);
1058                        }
1059                    }
1060                    Err(_) => best_result = Ok(solution),
1061                }
1062            }
1063        }
1064
1065        if let Ok(ref mut solution) = &mut best_result {
1066            solution
1067                .stock_pieces
1068                .sort_by_key(|p| cmp::Reverse((p.width, p.length)));
1069        };
1070
1071        best_result
1072    }
1073
1074    fn optimize_with_stock_pieces<B, F>(
1075        &self,
1076        stock_pieces: &[StockPiece],
1077        progress_callback: &F,
1078    ) -> Result<Solution>
1079    where
1080        B: Bin + Clone + Send + Into<ResultStockPiece>,
1081        F: Fn(f64),
1082    {
1083        let cut_pieces: Vec<&CutPieceWithId> = self.cut_pieces.iter().collect();
1084
1085        let units: Vec<OptimizerUnit<B>> = OptimizerUnit::generate_initial_units(
1086            stock_pieces,
1087            cut_pieces,
1088            self.cut_width,
1089            self.random_seed,
1090        )?;
1091
1092        let population_size = units.len();
1093        let mut result_units = Population::new(units)
1094            .set_size(population_size)
1095            .set_rand_seed(self.random_seed)
1096            .set_breed_factor(0.5)
1097            .set_survival_factor(0.6)
1098            .epochs(100, progress_callback)
1099            .finish();
1100
1101        let best_unit = &mut result_units[0];
1102        if !best_unit.unused_cut_pieces.is_empty() {
1103            return Err(no_fit_for_cut_piece_error(
1104                best_unit.unused_cut_pieces.iter().next().unwrap(),
1105            ));
1106        }
1107
1108        let fitness = best_unit.fitness();
1109        let price = best_unit.bins.iter().map(|bin| bin.price()).sum();
1110
1111        let used_stock_pieces: Vec<ResultStockPiece> =
1112            best_unit.bins.drain(..).map(Into::into).collect();
1113
1114        Ok(Solution {
1115            fitness,
1116            stock_pieces: used_stock_pieces,
1117            price,
1118        })
1119    }
1120}